diff --git a/.github/workflows/firmware_build.yml b/.github/workflows/firmware_build.yml
index fac245d8..2a5a385f 100644
--- a/.github/workflows/firmware_build.yml
+++ b/.github/workflows/firmware_build.yml
@@ -50,6 +50,7 @@ jobs:
"Adafruit BusIO" \
"Adafruit DRV2605 Library" \
"Adafruit INA228 Library" \
+ "Adafruit LIS3DH" \
"Adafruit LSM6DS" \
"Adafruit MAX31865 library" \
"Adafruit MCP9808 Library" \
@@ -59,8 +60,7 @@ jobs:
"Adafruit Unified Sensor" \
"Adafruit VCNL4040" \
"Adafruit VEML7700 Library" \
- "HX711" \
- "SparkFun LIS3DH Arduino Library"
+ "HX711"
- name: Compile every sketch
# ALLOW_EMPTY_PRODUCT_UID lets sketches that gate compilation on a
diff --git a/51-rooftop-hvac-predictive-maintenance/README.md b/51-rooftop-hvac-predictive-maintenance/README.md
index 47766be7..a49f4358 100644
--- a/51-rooftop-hvac-predictive-maintenance/README.md
+++ b/51-rooftop-hvac-predictive-maintenance/README.md
@@ -1,381 +1,5 @@
-# Rooftop HVAC Unit Predictive Maintenance Pack
+# Rooftop HVAC Predictive Maintenance
-
+A retrofit pack for commercial rooftop HVAC units that catches refrigerant loss, short-cycling, and clogged filters before the unit fails, using sensors and a cellular Notecard.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Rooftop%20HVAC%20Unit%20Predictive%20Maintenance%20Pack).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/rtu_predictive_maintenance/](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](https://blues.com/downtime-prevention/) pack for commercial rooftop HVAC units. A handful of inexpensive sensors and a Blues [Notecard Cell+WiFi](https://shop.blues.com/products/notecard-cellular?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) turn an ordinary rooftop unit into a remotely-monitored, predictively-maintained asset — catching 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 that samples four sensors every minute, runs three failure-mode checks locally, and pages your on-call queue within ~60 seconds of a threshold trip — over cellular, with no site IT involvement and no modification to the unit itself. Hourly summaries are emitted on a separate event stream for trending and analytics. Operators can re-tune thresholds in the field from the cloud, with no firmware changes required.
-
-## 1. Project Overview
-
-**The problem.** In HVAC, an **RTU** (rooftop unit) is a self-contained packaged HVAC system — compressor, condenser, evaporator, blower, and controls all in one box — mounted on the roof of a commercial building. It's the workhorse of light commercial cooling: the vast majority of 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, cannot see building WiFi, and when it fails a grocery store or restaurant loses cooling and goes dark. The HVAC failures that cause most unplanned downtime aren't dramatic — they're gradual. A refrigerant leak slowly reduces cooling capabilities over weeks. A tired contactor starts chattering, and the compressor short-cycles several times an hour instead of running in long, steady pulls. A neglected filter 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. Each one is an impending failure detectable 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 gets strapped to the RTU chassis on the roof, samples four sensors a 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: continuous remote monitoring on the edge, rule-based failure detection in firmware, and proactive-service alerts routed through [Blues Notehub](https://blues.com/notehub/) to whatever on-call system the fleet owner already uses. The detection includes three scalar threshold checks and catches impending-failure patterns HVAC technicians already 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 a single SKU that works identically in a strip mall and in a warehouse. Cellular removes the per-site IT involvement entirely — there's no network form to fill out, no AP to pair to, and no IT ticket to chase. The Notecard Cell+WiFi variant keeps WiFi as an optional fallback for the occasional site that happens to have a rooftop-accessible AP, without compromising the cellular-first deployment model.
-
-
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Inside the sidecar, the Cygnet STM32 host on the Notecarrier CX wakes once a minute, sweeps the four sensors, and runs the three failure-mode checks before the rooftop unit has time to make a meaningful change. When it's done, it hands the resulting Note (i.e. a JSON payload) to the Notecard over I²C and uses [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to cut its own power until the next interval. No JSON marshaling, no AT commands, no serial buffers to babysit — the host is 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](https://dev.blues.io/api-reference/glossary/#note) store. Summary records ride out on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes); any alert tagged `sync:true` skips the queue and the cellular radio comes up immediately to ship it. The same Notecard also handles one-time GNSS site geolocation autonomously and pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) (i.e. cloud variables) on every inbound sync, which is how a fleet operator re-tunes 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, and from there events flow into [Notehub](https://notehub.io), which ingests them, stores every one, and applies the project's routes to optionally sync data to your cloud platform of choice. Alerts and summaries land in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) on purpose — that lets you 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 without any filtering logic in the route itself.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, Twilio, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — this project doesn't ship any specific downstream endpoint, but it is trivial to route data to your selected cloud endpoint.
-
-## 3. Technical Summary
-
-If you want the fastest path from "parts on the bench" to "first event in Notehub":
-
-1. **Notehub** — create a free [Notehub project](https://notehub.io), copy its ProductUID.
-2. **Wire the bench rig** — [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) + [Notecard MBGLW](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) + two thermistor dividers on A0/A1 + CT bias network on A2 + SDP810 on I²C. Full pinout is in [§5](#5-wiring-and-assembly).
-3. **Edit one line** of [`firmware/rtu_predictive_maintenance/rtu_predictive_maintenance.ino`](firmware/rtu_predictive_maintenance/rtu_predictive_maintenance.ino) — set `PRODUCT_UID` to your project's value (line 24).
-4. **Flash** — `arduino-cli compile -b STMicroelectronics:stm32:Cygnet` then `arduino-cli upload`. Full instructions in [§7.1](#71-installing-and-flashing).
-5. **Watch** — open Notehub → your project → **Events** tab. You should see a `_session.qo` within minutes, an `rtu_summary.qo` within an hour, and any threshold trips as `rtu_alert.qo` in real time.
-
-The rest of this document expands each step and explains why the firmware is shaped the way it is.
-
-
-
-If you're doing a real rooftop install rather than a bench bring-up, also read [§11 Limitations](#11-limitations-and-next-steps) before you commit to a power topology.
-
-
-
-Here is a sample Note this device emits:
-
-```json
-{
- "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](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier board with an embedded Cygnet STM32 host — no separate MCU needed for this sensor mix. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) / [datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) | 1 | Cellular removes per-site IT involvement; WiFi fallback is available for sites that happen to have it. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(optional, bench-only)* | 0–1 | Coulomb counter on the power rail for ground-truth energy validation during bench bring-up. See [§9](#9-validation-and-testing) and [§11](#11-limitations-and-next-steps). |
-| 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 (e.g. [SparkFun SEN-11005](https://www.sparkfun.com/products/11005)) (current-output CT; add external burden resistor for 1 V_rms scaling) | 1 | Non-invasive compressor current sensing; 30A matches single-phase light-commercial RTU compressors. |
-| TRRS 3.5 mm breakout (e.g. [SparkFun BOB-11570](https://www.sparkfun.com/products/11570)) | 1 | The SCT-013's output lead terminates in a TRRS plug. |
-| 10 kΩ 1% resistor (bias pair) | 2 | CT bias circuit — centers the AC signal at Vref/2 so the ADC sees only positive voltages. |
-| 10 µF electrolytic capacitor | 1 | Bias-circuit decoupling. |
-| [Sensirion SDP810-125Pa](https://sensirion.com/products/catalog/SDP810-125Pa) differential-pressure sensor | 1 | I²C DP sensor across the filter; a continuous reading trends *toward* clogging, unlike a binary DP switch that 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-03-5](https://www.meanwell.com/Upload/PDF/IRM-03/IRM-03-SPEC.PDF)) | 1 | Derives 5V DC from 120VAC line power at the RTU service disconnect for permanent grid-tied power. See [Limitations](#11-limitations-and-next-steps) for the 24VAC-input alternative. |
-| NEMA 4X enclosure, ~6×4×2″ | 1 | Rooftop-rated housing. |
-
-All Blues parts ship with an embedded SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi (MBGLW) seats into the carrier's M.2 slot; cellular, GNSS, and WiFi antennas are u.FL leads to roof-mounted externals. The Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad, and reports cumulative mAh to the Notecard over its [Qwiic](https://www.sparkfun.com/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 with the midpoint (the **bias node**) sitting at Vref/2 ≈ 1.65 V. The 10 µF capacitor connects from the bias node to GND — it decouples the bias rail to AC ground, **not** in series with the signal. The CT's TRRS plug bridges A2 and the bias node: tip → A2, sleeve → bias node (or vice versa; the AC signal is symmetric). A2 then sees 1.65 V DC with the CT's AC swing superimposed, keeping the signal inside the 0–3.3 V ADC window.
-- **SDA / SCL** → SDP810 `SDA` / `SCL` (the Notecarrier has pull-ups on-board).
-- **+VBAT / +5V** pad → Mojo `LOAD` output; `BAT` input of Mojo ← 5V DC from the AC/DC supply.
-
-Mount probes inside the unit: supply thermistor ~12″ past the evaporator coil in the supply duct, return thermistor in the return plenum upstream of the filter, CT clamped on either of the hot legs feeding the compressor contactor (clamping only **one** leg is required, both will cancel), SDP810 tubes tapped across the filter frame with silicone tubing.
-
-
-
-**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 is still a safe "compressor running" floor. For larger units the threshold is conservative and you may want to raise it via the `compressor_on_amps` env var to better separate "running" from "starting transient." See [§6](#6-notehub-setup).
-
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) — it looks like `com.your-company.your-name:rtu-pdm`.
-2. **Set the ProductUID in firmware.** Open [`rtu_predictive_maintenance.ino`](firmware/rtu_predictive_maintenance/rtu_predictive_maintenance.ino) and replace the empty string on the `#define PRODUCT_UID ""` line (around line 24) with your value. Alternative: pass it as a build flag (`-DPRODUCT_UID=\"com.your-company.your-name:rtu-pdm\"`) if you'd rather not edit the source.
-3. **Claim the Notecard.** Power the assembled unit. On first cellular connect the Notecard associates itself with your Notehub project automatically — no manual claim step required. The device will appear in your project's **Devices** tab within a minute or two.
-4. **Create a Fleet.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) (and [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules)) are how Notehub groups devices for config and routing. One fleet per service territory is a reasonable starting point — it lets you set [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) (thresholds) at the fleet level and override per-device if a given unit turns out to be unusual.
-5. **Set environment variables.** In Notehub, navigate to **Fleet → Environment** (or **Device → Environment** for a one-off override). Add any of the variables below; the device pulls them on its next wake (no reflash, no truck roll). All are optional — firmware defaults are shown.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `delta_t_min_f` | `12.0` | Supply/return delta-T °F below which `delta_t_low` fires (only evaluated while the compressor is actually running). |
- | `short_cycle_starts_per_hour` | `8` | Compressor starts per rolling hour above which `short_cycling` fires. |
- | `filter_dp_alert_pa` | `90.0` | Filter differential pressure (Pa) above which `filter_dp_high` fires. |
- | `compressor_on_amps` | `3.0` | Amps above which the compressor is considered running. Raise for larger 5+ ton units, lower only with care — the value also gates compressor-runtime accounting. |
- | `sample_interval_sec` | `60` | Seconds between samples. |
- | `summary_interval_min` | `60` | Minutes between summary Notes. Changing this also re-applies `hub.set` so the Notecard's outbound transmit cadence tracks the new value. |
-
-6. **Configure routes.** At a minimum, add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `rtu_alert.qo` (to an on-call or CMMS endpoint) and a second for `rtu_summary.qo` (to a long-term store). Separating the two Notefiles at the source means you can fan them out to different destinations at different urgencies without any filtering in the route itself.
-
-
-
-Consult the guide on [configuring a Twilio route](https://dev.blues.io/notehub/messaging-and-data-pipelines/configuring-a-twilio-route/) to set up SMS messaging.
-
-
-
-### What you should see in Notehub
-
-Within a minute of first power-on, the **Events** tab in your project should start populating. Three event kinds matter for this project:
-
-- **`_session.qo`** — automatic Notecard housekeeping events on each cellular session. Useful sanity check that the radio is reaching Notehub at all.
-- **`rtu_summary.qo`** — one per `summary_interval_min`. The body looks like:
- ```json
- {
- "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
- }
- ```
- Any field reading `-9999` means "no valid samples for this metric in the window" — treat as a sensor fault, not a near-zero measurement.
-- **`rtu_alert.qo`** — only emitted on a threshold trip, transmitted immediately. The `alert` field is one of `delta_t_low`, `short_cycling`, or `filter_dp_high`; the remaining fields carry the supporting numbers. See [§8](#8-data-flow) for the per-alert payload shapes.
-
-## 7. Firmware Design
-
-Single sketch: [`firmware/rtu_predictive_maintenance/rtu_predictive_maintenance.ino`](firmware/rtu_predictive_maintenance/rtu_predictive_maintenance.ino).
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Install via the Arduino Boards Manager (search "STM32 MCU based boards") or by adding the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**. The Notecarrier CX's onboard host is a Cygnet-class STM32L4 — select **Generic STM32L4 series → Cygnet** as the board.
-- **`Blues Wireless Notecard`** library — [`note-arduino`](https://github.com/blues/note-arduino). Install via the Arduino Library Manager, or `arduino-cli lib install "Blues Wireless Notecard"`. Pin whatever current stable release the Library Manager offers at install time. See the [note-arduino releases](https://github.com/blues/note-arduino/releases) for the latest.
-
-**Flashing — Arduino IDE:** open `rtu_predictive_maintenance.ino`, select the Cygnet board, hit **Upload**. The Notecarrier CX exposes the ST-Link interface on the same USB cable, so no external programmer is needed.
-
-**Flashing — `arduino-cli`:** from the firmware directory,
-
-```bash
-# List installed STM32 boards to find the right FQBN for your installed core version
-arduino-cli board listall | grep -i cygnet
-
-# Then compile + upload (replace the FQBN below with what `listall` reported)
-arduino-cli compile -b STMicroelectronics:stm32:GenL4:pnum=CYGNET rtu_predictive_maintenance.ino
-arduino-cli upload -b STMicroelectronics:stm32:GenL4:pnum=CYGNET -p /dev/cu.usbmodem* rtu_predictive_maintenance.ino
-```
-
-The exact FQBN is whatever the current `stm32duino` core ships for the Cygnet variant — the `board listall` command above is the authoritative source for your specific core version. Replace `/dev/cu.usbmodem*` with whatever port the Notecarrier enumerates as on your machine, typically `COMx` on Windows, `/dev/ttyACM*` on Linux.
-
-After upload, open the serial monitor at **115200 baud** to watch the `[sample]` lines fly by. The first wake will print `[sample]` once, hand off to the Notecard, and then the host powers off until the next interval, so don't be alarmed when the serial 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 (per wake) | `fetchEnvOverrides` |
-| Thermistor / CT / SDP810 reading | `readThermistorF`, `readCompressorAmps`, `readFilterDpPa` |
-| Threshold evaluation and alert emission | `runSampleCycle`, `sendAlert` |
-| Hourly summary | `sendSummary` |
-| Persistent state across sleep cycles | `PersistState` + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### 7.3 Sensor reading strategy
-
-- **Thermistors.** 16-sample average of 12-bit ADC counts, convert to resistance via the divider ratio, convert to temperature via the β equation, convert to Fahrenheit.
-- **CT.** The SCT-013-030 emits an AC signal. A 2-resistor divider with a 10 µF capacitor biases the signal to Vref/2 so the ADC sees only positive voltages. The firmware derives 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.** I²C continuous measurement mode started 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 — don't mix the two up, the two variants share the same part family name.
-
-### 7.4 Event payload design
-
-One [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) summary Note (`rtu_summary.qo`) shipped hourly; untemplated alerts (`rtu_alert.qo`) shipped immediately via `sync:true`. Templates matter here because an RTU pack running for 10 years on 500 MB has to respect its data budget — templated Notes travel as fixed-length records, not free-form JSON, and shrink the wire size by roughly 3–5×.
-
-A quick refresher on the [Note template format](https://dev.blues.io/api-reference/notecard-api/note-requests/#note-template), which the snippet below uses: each numeric placeholder is a magic number whose integer part declares the wire type and whose fractional part declares the precision. `14.1` is a 4-byte IEEE-754 float kept to 1 decimal place; `12` is a 2-byte signed integer; `11` is a 1-byte signed integer; `18` is a 64-bit float. The Notecard rejects any subsequent `note.add` whose body doesn't match the registered shape, which catches firmware mistakes at the device rather than days later in your downstream pipeline.
-
-Example alert body:
-
-```json
-{
- "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, we still keep the host asleep most of the time — less heat in the enclosure, less wear on the supply, and a firmware pattern that ports directly to battery- or solar-powered variants without a rewrite. After each sample cycle the host calls `NotePayloadSaveAndSleep`, a `note-arduino` helper that serializes the in-RAM `PersistState` struct into Notecard flash and then issues a [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) request configured to cut host power entirely for `sample_interval_sec` seconds. When ATTN re-fires, the Notecarrier re-applies host power, the MCU enters `setup()` from cold, and `NotePayloadRetrieveAfterSleep` pulls the saved struct back. From the firmware author's perspective, the sleep call looks like a single line — the Notecard does the rest.
-
-The Notecard itself sits in its own [low-power idle](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) (~8 µA @ 5V) between cellular wakes. Sample rate and transmit rate are deliberately decoupled: we sample every 60 seconds but only transmit once an hour — alerts are the only thing that bypass the transmit timer.
-
-### 7.6 Retry and error handling
-
-- The first Notecard transaction uses [`sendRequestWithRetry(req, 10)`](https://dev.blues.io/tools-and-sdks/firmware-libraries/arduino-library/) to paper over the cold-boot I²C race that the `note-arduino` library docs warn about.
-- Sensor reads returning `NaN` (unplugged probe, ADC rail pegged) are excluded from summary averages on a per-metric basis — each metric carries its own valid-sample counter so a single bad SDP810 read doesn't bias the thermistor averages. If a metric has zero valid samples in the window, the summary emits `-9999` as a sentinel rather than a misleading zero.
-- The compressor-on branch also guards against evaluating delta-T when the unit is not actually cooling, so a perfectly-fine RTU idling overnight doesn't page anyone.
-- Env var changes to `summary_interval_min` re-apply `hub.set` on the next wake, so the Notecard's outbound cellular cadence stays in sync with local summary cadence rather than drifting apart.
-- Alert de-duplication via `ALERT_COOLDOWN_SEC` (30 minutes) prevents a slow-drifting sensor from paging the on-call every single sample — one alert per 30 minutes per failure mode is plenty.
-
-### 7.7 Key code snippet 1: template definition
-
-The template makes each summary Note a fixed-length record on the wire. `14.1` means 4-byte float; `12` means 2-byte signed integer.
-
-```cpp
-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.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "rtu_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "delta_t_low");
-JAddNumberToObject(body, "delta_t_f", delta_t);
-JAddNumberToObject(body, "supply_f", supply);
-JAddNumberToObject(body, "return_f", ret);
-notecard.sendRequest(req);
-```
-
-### 7.9 Key code snippet 3: sleep between samples
-
-`NotePayloadSaveAndSleep` persists state to the Notecard and then triggers `card.attn` which cuts host power. The next wake enters `setup()` fresh; `NotePayloadRetrieveAfterSleep` rehydrates the state.
-
-```cpp
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, SAMPLE_INTERVAL_SEC, NULL);
-```
-
-## 8. Data Flow
-
-
-
-Every 60 seconds the firmware runs one sample cycle, evaluates three independent threshold checks in parallel, and emits an alert for each one that trips. All three alerts converge onto the same `rtu_alert.qo` Notefile with `sync:true`, so any one failure mode paging out doesn't depend on the state of the other two.
-
-- **Collected.** Supply-air °F, return-air °F, compressor RMS amps, filter DP in Pa, compressor starts in the last rolling hour, cumulative compressor runtime.
-- **Transmitted.**
- - `rtu_summary.qo` — one record every `summary_interval_min` (default 60 minutes), template-encoded. 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` — emitted only on a threshold trip, immediate sync, 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 deliberately separate so you can fan them out differently.
-- **Alerts trigger on.**
- - `delta_t_low` — cooling delta-T below `delta_t_min_f` while the compressor is drawing current (refrigerant loss indicator). Body: `{ alert, delta_t_f, supply_f, return_f }`.
- - `short_cycling` — more than `short_cycle_starts_per_hour` compressor starts inside a true rolling 60-minute window (ring buffer of recent start timestamps). Body: `{ alert, starts_per_hour, compressor_amps }`.
- - `filter_dp_high` — filter DP above `filter_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 every minute. The three trips are independent: a unit can simultaneously be short-cycling *and* losing refrigerant, and you'll see one alert of each kind.
-
-## 9. Validation and Testing
-
-**Expected cadence after deployment.** In steady state, a correctly-behaving RTU generates one `rtu_summary.qo` event per hour and zero `rtu_alert.qo` events. Expect occasional alerts during the first week while you dial in thresholds for the specific unit — thermistor placement, duct geometry, and compressor model all nudge the baseline around.
-
-**Using Mojo to validate power behavior (optional bench step).** The Notecard's published idle figure is roughly 8 µA @ 5V. See the [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for the authoritative measured numbers across Notecard SKUs and modes. Transmit bursts for a single small queued Note are on the order of tens of seconds at hundreds of mA peak on LTE Cat-1 bis, dominated by the radio warm-up and network registration. The host MCU, when cut by `card.attn`, draws essentially zero from the +VBAT rail.
-
-A useful concrete target: with the defaults in this sketch (60-second sample, hourly transmit, host fully off between samples), expect on the order of **25–40 mAh / 24 h** in steady state on cellular, dominated by the once-per-hour radio burst. Deviations of more than ~3× from that envelope are usually one of two things: the host isn't actually sleeping (continuous tens-of-mA baseline) or the radio is camping in registration (longer-than-expected hourly bursts on weak signal). Both are visible on a Mojo trace before they're visible anywhere else.
-
-The [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) reports cumulative mAh to the Notecard at 1% accuracy over its Qwiic link. A useful bench-up exercise: leave a unit running for 24 h powered from a known battery and confirm the Mojo tally lines up with the expected pattern — "host awake for a few seconds every minute, radio on for tens of seconds once an hour." That pattern is what you should see on a Mojo trace:
-
-- **Healthy:** flat baseline near zero, brief sub-second blips at the sample interval, one ~10–30-second burst at 100–300 mA per hour.
-- **Host never sleeping:** flat ~80–150 mA continuous baseline. Almost always a `card.attn` wiring or firmware regression (`NotePayloadSaveAndSleep` returning early, host code re-entering `loop()` repeatedly).
-- **Weak signal / radio struggling:** correctly-spaced hourly bursts but each burst is 60 seconds+ at peak. Move the cellular antenna or check site coverage.
-
-Because the deployed RTU is grid-tied through the 120VAC service-disconnect supply, the absolute mAh number matters less in production than the *shape* of the trace. The bench measurement is still worth doing once per firmware revision — it's the cheapest way to catch a regression that silently keeps the host awake.
-
-## 10. Troubleshooting
-
-A short field guide for the things that actually go 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. | Re-verify `PRODUCT_UID` matches the Notehub project exactly. Move the unit outside or near a window. Check `_session.qo` events — none means no cellular connection. |
-| Device appears, `_session.qo` events arrive, but no `rtu_summary.qo` ever shows up. | All sensor reads are returning `NaN`, so `sendSummary` short-circuits via the `any_valid` guard. | Open the serial monitor at 115200 baud and watch the `[sample]` lines. Any field reading `nan` indicates 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` rather than misleading numbers. Check the divider wiring at A0 / A1. |
-| `compressor_amps` always 0 even with the compressor running. | CT is on the neutral leg or both hot legs (currents cancel), or the bias network is missing the 10 µF cap. | Re-clamp the CT on **one** hot leg only. Verify 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 pull-ups — the Notecarrier CX has them, but breakouts in line may add more. |
-| Alerts firing constantly on a healthy unit. | Thresholds aren't tuned for this unit yet. | Raise `delta_t_min_f` toward 14–15 °F if the supply duct runs short, 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. | Host isn't sleeping — `card.attn` isn't gating power. | Confirm you're on a Notecarrier CX (which supports ATTN host gating); see [§9](#9-validation-and-testing) for trace shapes. |
-
-
-
-If a problem isn't on this list, the [Blues community forum](https://discuss.blues.com) is generally the fastest place to get a second pair of eyes on a Notecard + sensor setup.
-
-
-
-## 11. Limitations and Next Steps
-
-This pack is deliberately a starting point — small, retrofittable, and aimed at the failure modes a single technician already knows how to recognize. 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
-
-The following are scope choices, not oversights — each trades a capability a fuller platform would want against the simplicity that makes this pack retrofittable by a single technician.
-
-**Detection is rule-based.** The firmware runs three scalar threshold checks, not a trained model. That catches the impending-failure patterns HVAC technicians already recognize on a service call, but it isn't the anomaly-detection or drift-analysis that a more sophisticated predictive-maintenance platform would layer 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**, which means a communication-layer fault on the I²C bus won't be caught.
-
-**Compressor-start detection is sampled, not interrupt-driven.** Transitions that occur 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 sees.
-
-**No local storage of historical samples.** The Notecard does its own multi-day Note queue, but the sketch itself keeps 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.** Installations that can only offer the 24VAC control-transformer rail need a 24VAC-input supply instead (for example, a Functional Devices PSH40A or a Mornsun LH05-23B05R3); the downstream 5V wiring is unchanged. A Scoop / solar variant is a reasonable addition for units where neither rail is practical.
-
-**Mojo is bench-validation equipment in this POC.** The firmware does not read its LTC2959 coulomb counter over the Qwiic bus. Adding a runtime mAh field to the summary is a straightforward extension if fleet-level power telemetry is valuable.
-
-### Production Next Steps
-
-Once the basic detector is paying for itself, a handful of extensions make sense — roughly from the most immediately useful to the most infrastructure-dependent.
-
-**Three-phase CT support** (or 208/240V split-phase) brings larger units into scope: a second and third CT on A3/A4 plus an RMS sum in firmware.
-
-**A duct-static-pressure sensor on the supply side** would distinguish "clogged filter" from "blocked coil." The two look similar from the return side alone, 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 low-hanging fruit — the bytes are already there in the frame, the firmware just ignores them today.
-
-**Field-upgradeable firmware via [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** would let a service company push a threshold recipe or a new failure-mode detector to the entire fleet without a truck roll.
-
-**Per-unit commissioning** via a probe-offset correction environment variable tightens accuracy: thermistors drift a few degrees with duct placement, and calibrating once at install time is strictly better than living with the noise.
-
-**A weather-input overlay** correlates delta-T against outside-air temperature — via a Notehub environment variable sourced from a weather API route, or a dedicated outdoor-air probe — so refrigerant-loss detection isn't falsely triggered 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 a page hours earlier: refrigerant slowly leaking, a contactor starting to chatter, a filter approaching clog. 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 — and the same firmware drops onto the next rooftop, and the one after that, without an IT ticket or a site visit.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/rooftop-hvac-predictive-maintenance/)**
diff --git a/51-rooftop-hvac-predictive-maintenance/banner.png b/51-rooftop-hvac-predictive-maintenance/banner.png
deleted file mode 100644
index d0ad628c..00000000
Binary files a/51-rooftop-hvac-predictive-maintenance/banner.png and /dev/null differ
diff --git a/51-rooftop-hvac-predictive-maintenance/diagrams/01-system-architecture.svg b/51-rooftop-hvac-predictive-maintenance/diagrams/01-system-architecture.svg
deleted file mode 100644
index 7ae8004f..00000000
--- a/51-rooftop-hvac-predictive-maintenance/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-
diff --git a/51-rooftop-hvac-predictive-maintenance/diagrams/02-wiring-assembly.svg b/51-rooftop-hvac-predictive-maintenance/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index fcf68238..00000000
--- a/51-rooftop-hvac-predictive-maintenance/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,95 +0,0 @@
-
diff --git a/51-rooftop-hvac-predictive-maintenance/diagrams/03-data-flow.svg b/51-rooftop-hvac-predictive-maintenance/diagrams/03-data-flow.svg
deleted file mode 100644
index e164d542..00000000
--- a/51-rooftop-hvac-predictive-maintenance/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/52-vfd-pump-predictive-maintenance/README.md b/52-vfd-pump-predictive-maintenance/README.md
index feadfcc9..4b275552 100644
--- a/52-vfd-pump-predictive-maintenance/README.md
+++ b/52-vfd-pump-predictive-maintenance/README.md
@@ -1,407 +1,5 @@
-# VFD-Driven Pump Predictive Maintenance via Modbus
+# VFD Pump Predictive Maintenance
-
+A reference design that turns an industrial centrifugal pump into a predictively-maintained asset by reading its VFD over Modbus RTU with a cellular Notecard for the uplink.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=VFD-Driven%20Pump%20Predictive%20Maintenance%20via%20Modbus).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/vfd_pump_monitor/](firmware/vfd_pump_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [downtime prevention](https://blues.com/downtime-prevention/) reference design that turns an industrial centrifugal pump into a predictively-maintained, remotely-monitored asset by reading what the pump's existing **VFD (variable frequency drive)** already knows about itself over **Modbus RTU**, on a real industrial **PLC** (programmable logic controller), with a cellular [Notecard](https://shop.blues.com/products/notecard-cellular?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) for the data uplink.
-
-## 1. Project Overview
-
-**The problem.** Industrial centrifugal pumps almost universally run behind a VFD. Modern drives from ABB, Yaskawa, Danfoss, and Schneider all expose the pump's operating telemetry through Modbus holding registers — motor current, output frequency, output torque, drive temperature, runtime hours, active fault code. Almost nobody reads them. The VFD is sitting in the cabinet doing the work; the data is right there. What's missing is the network path off the plant floor.
-
-A failing pump rarely just stops. It signals first: motor current can shift at constant frequency as bearing drag, fouling, valve position, fluid viscosity, or impeller condition change the load on the drive. Transient electrical faults can cluster on a stressed contactor or in a cavitating hydraulic regime. Actual runtime can drift above expected duty cycle as a downstream valve fouls and the pump runs longer to do the same work. *None of these telemetry shifts are diagnostic by themselves*, but they are leading indicators a maintenance team would happily act on a week early. This project is the device that catches those anomalies and routes them out, before a tank runs dry on a Saturday night. See the [Signal limitations](#12-limitations-and-next-steps) note for what this telemetry can and cannot conclude.
-
-**Why Notecard.** Pump rooms sit on isolated OT (operational technology) networks where corporate WiFi is off-limits for instrumentation by plant policy, and retrofitting a separate OT-friendly access point per pump is unrealistic. Cellular removes the dependency on plant LAN credentials, VLAN provisioning, site WiFi, or a local gateway. (Some sites will still require OT/security review before *any* wireless instrumentation is allowed; the device just doesn't need access to the plant network itself.) The Notecard's bundled global cellular data and service means the same firmware and architecture deploys across regions — choose the matching [Blues Wireless for OPTA](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) cellular variant for the deployment geography. WiFi remains as an opportunistic fallback for the rare site that *can* offer a rooftop AP, without compromising the cellular-first model.
-
-
-
-**Why OPTA.** This is exactly the sweet spot for Wireless for OPTA: a real industrial PLC reading a real industrial Modbus device, with no PC or gateway in the middle. The [Arduino OPTA](https://www.arduino.cc/pro/hardware-arduino-opta/) is DIN-rail-mounted, 12–24 VDC powered, and programmable with both Arduino sketches and IEC 61131-3 PLC languages, so it deploys exactly the same way across plants whose IT and OT teams will never agree on a network plan. Pairing it with Blues Wireless for OPTA adds cellular and WiFi to a device that already speaks the language of the plant.
-
-**Deployment scenario.** A single OPTA + Wireless for OPTA mounted on the DIN rail next to the VFD inside the pump's electrical panel, RS-485 daisied to the drive's communication port, antenna routed out through a cable gland to a magnetic-mount whip on the cabinet roof. Line power from a 24 VDC panel supply that's already there. No PC, no gateway, no plant LAN involvement.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Inside the pump panel, the OPTA's Cortex-M7 plays Modbus RTU **client** (master) to the VFD's **server** (slave), polling six holding registers across the onboard RS-485 transceiver once a minute. Between polls it accumulates rolling hourly statistics — mean, peak, count — in RAM and runs four anomaly-detection rules against them, deciding event-by-event whether the plant cares about what it just saw. When something is worth reporting, the host hands a JSON [Note](https://dev.blues.io/api-reference/glossary/#note) to the Notecard over I²C through Blues Wireless for OPTA's AUX connector. No modem AT commands, no session state machine, no raw socket management — the `note-arduino` library's `JAdd*` helpers build the request and the Notecard takes it from there.
-
-**Notecard responsibilities.** The Notecard's on-device queue holds every Note the host writes, then ships them out on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default hourly). Anything tagged `sync:true` jumps the queue and the radio comes up immediately. The same module also pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) on every inbound sync, which is what lets a maintenance lead retune thresholds — or even point the firmware at a different drive vendor's register addresses — from a browser, without anyone visiting the panel.
-
-**Notehub responsibilities.** [Blues Notehub](https://blues.com/notehub/) is where the events land — every one ingested, every one stored, project-level [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) fanning them onward. Because every plant's VFDs may sit at different register addresses, the per-fleet [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) story is what keeps a single firmware image servicing an ABB plant in Ohio and a Yaskawa plant in São Paulo. See [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) for how to organize them by vendor or site.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for more information.
-
-## 3. Technical Summary
-
-**What you'll have when done:** An OPTA + Notecard assembly mounted in a pump panel, reading six telemetry registers from the VFD once per minute, reporting hourly summaries to Notehub, and emitting immediate alerts when four anomaly rules trigger.
-
-**Minimum steps** (60–90 minutes, assuming site access and a calibrated VFD):
-1. Install Arduino core + libraries (`Arduino Mbed OS Opta Boards`, `note-arduino`, `ArduinoModbus`, `ArduinoRS485`) via Library Manager.
-2. Set `PRODUCT_UID` in the firmware; compile and flash via `arduino-cli` (see [§8. Build and Flash](#8-build-and-flash) below).
-3. On Notehub: create project, claim Notecard, create one fleet, set `modbus_slave_id` and `modbus_baud` to match your VFD's configuration.
-4. Wire OPTA RS-485 to VFD Modbus port; confirm 120 Ω termination at each end of the bus.
-5. Power up and monitor via a serial monitor: `card.status` should report healthy; first `vfd_summary.qo` Note appears within ~60 s.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "f_hz_mean": 59.8, "f_hz_peak": 60.1,
- "i_a_mean": 12.4, "i_a_peak": 13.7,
- "trq_mean": 68, "trq_peak": 74,
- "drv_c_mean": 42, "drv_c_peak": 51,
- "run_min": 53, "stop_min": 7,
- "hrs_total": 18234,
- "fault_count_hour": 0,
- "last_fault": "0"
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Arduino OPTA RS485](https://store.arduino.cc/products/opta-rs485) | 1 | Industrial PLC with onboard RS-485 transceiver, DIN-rail mount, 12–24 VDC supply. Programmable with Arduino sketches or IEC 61131-3 PLC languages. Hosts the Modbus client and edge logic. |
-| [Blues Wireless for OPTA](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Snaps onto the OPTA's expansion port; adds a [Notecard Cell+WiFi (WBNAW)](https://dev.blues.io/datasheets/notecard-datasheet/note-wbnaw/) over I²C. Cellular coverage is regional — pick the matching SKU for the deployment geography. The Notecard manages its own cellular session against supported carrier networks worldwide via its embedded global SIM. |
-| External cellular antenna(s) w/ SMA, ~3m lead (e.g. [SparkFun CEL-16432](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html)) | 1 required, 2 recommended | At minimum, route the **primary** cellular antenna outside any metal cabinet; rubber-duck antennas inside a steel panel will not work reliably. For best LTE Cat-1 performance also route the **diversity** antenna externally if the layout allows it. SMA bulkhead leads through cable glands keep the IP rating intact. |
-| [Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter for validating the Wireless for OPTA + Notecard subsystem energy per session during commissioning. Not deployed to the field. |
-| 24 VDC DIN-rail supply, ≥10W (e.g. [MeanWell HDR-15-24](https://www.meanwell.com/Upload/PDF/HDR-15/HDR-15-SPEC.PDF)) | 1 | Powers OPTA and expansion. Most pump panels already have one — only buy if needed. |
-| 120 Ω termination resistor | 1–2 | RS-485 termination at each end of the bus. The OPTA's onboard transceiver does not include a permanent terminator. |
-| Shielded twisted pair, 22 AWG, RS-485 rated, ~1m | 1 | A → A, B → B, shield → drive ground. Length depends on the panel layout. |
-| DIN rail, ~10 cm | 1 | Mount for the OPTA + expansion. Likely already present in the panel. |
-
-
-
-The Arduino OPTA WiFi variant works equally well if WiFi/BLE on the host MCU is desired; the firmware is unchanged. The OPTA Lite that ships in the standard Wireless for OPTA bundle has **no onboard RS-485** and is not suitable for this project.
-
-
-
-The Blues hardware ships with an active SIM bundled with 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-
-
-**Safety.** VFD cabinets contain hazardous voltages even when control wiring is low-voltage. Installation must be performed by qualified personnel following site lockout/tagout procedures, the VFD manufacturer's instructions, and applicable electrical codes. This reference design is **read-only** over Modbus — it does not command pump start/stop or speed.
-
-
-
-1. **Mount and power.** Snap the OPTA onto the DIN rail, snap the Blues Wireless for OPTA onto the OPTA's right-hand expansion port. Use the supplied solderless AUX connector between the two — the connector carries the I²C lines that the Notecard rides on. Wire 24 VDC from the panel supply to the OPTA's `+` and `-` terminals. Per the [Wireless for OPTA Quickstart](https://dev.blues.io/quickstart/wireless-for-opta-quickstart/), the OPTA's outputs are not powered by USB-C, so use the 12–24 VDC supply for any deployment beyond bench testing. Jumper the OPTA's `+24V` to the expansion's `+24V` terminal so the expansion shares the same supply.
-2. **Antennas.** Thread an SMA-female bulkhead lead through a cable gland for the primary cellular antenna and screw it onto the first antenna port on Wireless for OPTA. If the layout allows, do the same for the diversity port — it improves LTE Cat-1 performance in marginal-signal sites. Don't rely on the bundled rubber-duck antennas inside a metal cabinet; they're for bench testing only.
-3. **Modbus bus.** Wire the OPTA's RS-485 terminals to the VFD's communication port:
- - OPTA `A (+)` → VFD `A (+)`
- - OPTA `B (-)` → VFD `B (-)`
- - OPTA `COM` (RS-485 GND) → VFD communication ground (consult the drive's manual; ABB ACS580 calls it `AGND`, Yaskawa GA500 calls it `SC`, Danfoss FC-302 calls it `Common`).
- - Place a 120 Ω resistor across A/B at *each* end of the bus. With one OPTA and one drive, that means two terminators total (one at each device).
-4. **Drive configuration.** Configure the VFD as a Modbus RTU **server** (slave); the OPTA acts as the Modbus **client** (master). Defaults the firmware ships with: slave ID `1`, baud rate `19200`, 8 data bits, no parity, 1 stop bit (`8N1`). Real drives vary — match baud, parity, stop bits, and slave address to whatever the VFD is configured for, and override the firmware's defaults via the `modbus_*` environment variables on Notehub. Common vendor parameter groups: ABB `5800–5805`, Yaskawa `H5-xx`, Danfoss `8-3x`, Schneider `Comm-1.x`.
-5. **Bench validation.** During first-light testing, splice the Mojo inline between the 24 VDC supply and the Wireless for OPTA power input so it can measure the *expansion + Notecard subsystem* energy per session. The OPTA itself is line-powered and not the subject of measurement.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [Notehub](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/vfd_pump_monitor/vfd_pump_monitor.ino` as `PRODUCT_UID`.
-2. **Claim the Notecard.** Power up the panel; on first cellular session the Notecard associates with your project automatically.
-3. **Create a Fleet per plant.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration and routing. The natural unit here is *one fleet per plant* — every pump in a plant typically has the same VFD vendor and the same register map, so fleet-level environment variables encode "this plant's pumps all run ABB drives at register 0x0103 for output frequency."
-4. **Set environment variables.** In Notehub: Projects → your project → Fleets → your fleet → Environment. Defaults below are reasonable starting points; any value set in Notehub overrides the firmware default on the device's next inbound sync. The register-address variables let one firmware image work across all four common drive vendors without recompilation.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_minutes` | `1` | Minutes between Modbus polls. |
- | `report_minutes` | `60` | Minutes between summary Notes (`vfd_summary.qo`). |
- | `modbus_slave_id` | `1` | Modbus server (slave) address of the drive. |
- | `modbus_baud` | `19200` | Bus baud rate; must match the VFD configuration. |
- | `modbus_parity` | `none` | Parity setting: `none`, `even`, or `odd`. Must match the VFD. |
- | `modbus_stop_bits` | `1` | Stop bits: `1` or `2`. Must match the VFD. |
- | `vfd_profile` | `demo_contiguous` | Placeholder identifying the register-map convention this firmware build targets. The shipped firmware only implements `demo_contiguous`. See [Limitations](#12-limitations-and-next-steps) for the production path. |
- | `reg_freq` | `259` | Holding-register address (wire-level, 0-based) for output frequency. Demo firmware assumes 0.01 Hz units. |
- | `reg_current` | `260` | Holding-register address for motor current. Demo firmware assumes 0.01 A units. |
- | `reg_torque` | `261` | Holding-register address for output torque. Demo firmware assumes % of nominal (signed 16-bit). |
- | `reg_drive_temp` | `262` | Holding-register address for drive heatsink temperature. Demo firmware assumes °C (signed 16-bit). |
- | `reg_runtime_hours` | `263` | Holding-register address for cumulative runtime hours. Demo firmware reads a single 16-bit register. |
- | `reg_fault_code` | `264` | Holding-register address for **active** fault code (0 = no fault). This is *not* a fault history log. See [Limitations](#12-limitations-and-next-steps). |
- | `current_alarm_factor` | `1.20` | Fires `load_anomaly` when hourly mean current exceeds the rolling baseline by this factor while running. |
- | `transient_fault_window_hours` | `4` | Window for transient-fault clustering. |
- | `transient_fault_count` | `3` | Distinct fault *transitions* within the window above which `transient_faults` fires. |
- | `drive_temp_alarm_c` | `75.0` | Drive heatsink °C above which `drive_overtemp` fires. |
- | `expected_run_hours_per_day` | `12.0` | Expected runtime per day; runtime drift > 25% above this fires `runtime_drift`. |
-
-
-
- **VFD register-map gotchas.** The defaults above are illustrative only. Real VFDs differ on: 0-based vs 1-based addressing conventions (Modicon "40001" notation vs raw); per-register scaling (current may be 0.1 A, 0.01 A, or % of rated); signedness (torque and temperature are often signed); 32-bit values that span two registers (runtime hours often does, with vendor-specific word order); and active-fault-code vs fault-history-log distinction. The shipped firmware reads six contiguous 16-bit registers with hardcoded scaling — production deployments need a vendor-specific firmware build with proper handling.
-
-
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `vfd_event.qo` (alerts, low-volume, real-time delivery to a CMMS or on-call endpoint) and a second for `vfd_summary.qo` (long-term storage, batched delivery to an analytics/historian system). Splitting the two Notefiles at the source means each can be fanned out to a different destination at a different urgency without filter logic in the route.
-
-## 7. Firmware Design
-
-Single sketch: [`firmware/vfd_pump_monitor/vfd_pump_monitor.ino`](firmware/vfd_pump_monitor/vfd_pump_monitor.ino).
-
-Dependencies:
-- **Arduino Mbed OS Opta Boards** core (install via the Arduino IDE Boards Manager).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (the `note-arduino` library). Install via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`.
-- [`ArduinoModbus`](https://github.com/arduino-libraries/ArduinoModbus) and [`ArduinoRS485`](https://github.com/arduino-libraries/ArduinoRS485) (official Arduino libraries, install via Library Manager).
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard configuration (`hub.set`, templates) | `notecardConfigure`, `defineTemplates` |
-| Environment-variable fetch + clamp | `fetchEnvOverrides` (`clampU32`, `clampF`) |
-| Re-init bus / re-issue `hub.set` on env change | `applyModbusSerialIfChanged`, `applyHubSetIfChanged` |
-| Modbus polling of six registers | `pollVfd`, `modbusReadOne` |
-| Rolling hourly stats | `RollingStats` struct, `accumulate` |
-| Edge logic (load anomaly, transient faults, runtime drift, drive overtemp) | `evaluateRules` (with frequency-binned baselines and edge-triggered alerts) |
-| Outbound Note emission (summary + event) | `sendSummary`, `sendEvent` |
-| Periodic cycle scheduler (no sleep, line-powered) | `loop()` |
-
-### Sensor reading strategy
-
-Six holding registers are pulled in a single Modbus transaction (`requestFrom` with `HOLDING_REGISTERS`, quantity 6, starting address taken from `reg_freq`). Reading all six in one transaction is roughly 8× cheaper in time and bus utilization than six individual reads. The firmware assumes the six registers are contiguous; on drives where they aren't, override `reg_*` variables individually and the firmware falls back to per-register reads.
-
-Polling cadence is `sample_minutes` (default 1 minute). For each sample, `accumulate` updates the rolling hourly windows for current, frequency, torque, and drive temperature — separately tracking samples taken while the pump is *running* (frequency > 1 Hz) versus *stopped*, since a hot drive at 0 Hz is a different signal than a hot drive under load.
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates give summary and alert records a stable schema, store as fixed-length records on the Notecard rather than free-form JSON, and minimize on-wire payload size — material at 24 summary Notes per day per pump over a multi-year deployment. Actual cellular data usage depends on sync cadence, signal conditions, routing behavior, and event frequency, so production deployments should validate usage with [Notehub usage data](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#viewing-usage-data-on-notehub).
-
-`vfd_summary.qo` (hourly):
-
-```json
-{
- "file": "vfd_summary.qo",
- "body": {
- "f_hz_mean": 59.8, "f_hz_peak": 60.1,
- "i_a_mean": 12.4, "i_a_peak": 13.7,
- "trq_mean": 68, "trq_peak": 74,
- "drv_c_mean": 42, "drv_c_peak": 51,
- "run_min": 53, "stop_min": 7,
- "hrs_total": 18234,
- "fault_count_hour": 0,
- "last_fault": "0"
- }
-}
-```
-
-`vfd_event.qo` (immediate, `sync:true`):
-
-```json
-{
- "file": "vfd_event.qo",
- "body": {
- "alert": "load_anomaly",
- "f_hz": 0,
- "i_a": 0,
- "v1": 59.8,
- "v2": 14.9,
- "v3": 12.3,
- "fault_code": 0,
- "hrs_total": 18234
- }
-}
-```
-
-The event template carries seven fixed fields. `f_hz` and `i_a` are populated when the alert is *triggered by a specific sample* (e.g. a Modbus failure event captures the last-known sample state); when the alert is the result of an hourly aggregate they're zero and the relevant aggregates land in three generic numeric slots `v1` / `v2` / `v3` whose meaning depends on the alert type:
-
-| Alert | `v1` | `v2` | `v3` |
-|---|---|---|---|
-| `load_anomaly` | hourly mean frequency | hourly mean current | bin baseline current |
-| `transient_faults` | distinct fault transitions in window | window hours | unused (0) |
-| `drive_overtemp` | hourly peak heatsink °C | hourly mean heatsink °C | unused (0) |
-| `runtime_drift` | observed daily runtime hours | expected daily runtime hours | unused (0) |
-| `modbus_unreachable` | unused (0) | unused (0) | unused (0) |
-
-Production builds may rename the firmware fields to alert-specific names (e.g. `i_a_mean`, `i_a_baseline`) at the cost of a per-alert template — the `v1`/`v2`/`v3` shape keeps a single shared template for all alert types and matches what the demo firmware emits.
-
-### Power and sync strategy
-
-The OPTA + expansion is line-powered (24 VDC), so MCU sleep is not the goal — bus and bandwidth efficiency is. The Notecard runs in [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `periodic` mode with `outbound:60` and `inbound:120` (minutes). Summary Notes accumulate in the on-device queue and ship in a single session every hour; alert Notes set `sync:true` and ship within a session-establishment window of the trigger (~15–60 seconds typical).
-
-### Retry and error handling
-
-- The first `hub.set` uses `notecard.sendRequestWithRetry()` with a 5-second window — there is a known cold-boot race condition where the host comes up before the Notecard is ready to receive I²C transactions.
-- Modbus reads are retried up to 3× per cycle on `lastError() != 0`. If all three fail, the firmware skips that sample (no NaN ever appears in any payload: JSON has no valid `NaN` literal, and templated Notes validate field types) and emits a separate `modbus_unreachable` event Note. The event is rate-limited to once per hour to avoid alarm fatigue when the drive itself is powered off for service.
-- Notecard requests use `notecard.requestAndResponse()` and check both `NULL` return and the `err` field on the response object before trusting the data.
-
-### Key code snippets
-
-Configuring the Notecard for periodic sync, with templates for both Notefiles, runs once at boot:
-
-```cpp
-J *req = notecard.newRequest("hub.set");
-JAddStringToObject(req, "product", PRODUCT_UID);
-JAddStringToObject(req, "mode", "periodic");
-JAddNumberToObject(req, "outbound", 60);
-JAddNumberToObject(req, "inbound", 120);
-notecard.sendRequestWithRetry(req, 5);
-```
-
-Polling the VFD and accumulating hourly stats — one Modbus transaction reads all six contiguous holding registers:
-
-```cpp
-if (!ModbusRTUClient.requestFrom(slaveId, HOLDING_REGISTERS, regBase, 6)) {
- return false; // caller handles retry / error event
-}
-sample.frequency_hz = ModbusRTUClient.read() / 100.0f;
-sample.current_a = ModbusRTUClient.read() / 100.0f;
-sample.torque_pct = (int16_t)ModbusRTUClient.read();
-sample.drive_temp_c = (int16_t)ModbusRTUClient.read();
-sample.runtime_hours = ModbusRTUClient.read();
-sample.fault_code = ModbusRTUClient.read();
-```
-
-The load-anomaly rule — rising current at *comparable* output frequency is one of the observables maintenance techs look for. The firmware buckets samples into 5 Hz frequency bins and tracks an EWMA-smoothed baseline current per bin, so 30 Hz operation isn't compared against a 60 Hz baseline. The alert is edge-triggered (fires once on the rising edge, rearms when the bin's mean returns below threshold):
-
-```cpp
-const uint8_t bin = freqBin(f_hz_mean);
-const float baseline = g_current_baseline_by_bin[bin];
-
-if (g_baseline_seeded[bin] &&
- i_a_mean > baseline * g_current_alarm_factor &&
- !g_active_load_anomaly) {
- sendEvent("load_anomaly", nullptr, f_hz_mean, i_a_mean, baseline);
- g_active_load_anomaly = true;
-}
-```
-
-## 8. Build and Flash
-
-**Prerequisites:** Arduino IDE (or `arduino-cli` on the command line) with the following installed via Boards Manager and Library Manager:
-- Boards: `Arduino Mbed OS Opta Boards`
-- Libraries: `Blues Wireless Notecard`, `ArduinoModbus`, `ArduinoRS485`
-
-**Steps:**
-
-1. Clone or download the repo and open `firmware/vfd_pump_monitor/vfd_pump_monitor.ino` in the Arduino IDE.
-
-2. Replace the placeholder `PRODUCT_UID` at the top of the sketch with your Notehub ProductUID:
- ```cpp
- #define PRODUCT_UID "prod.your-notehub-project-id"
- ```
- (Find it in Notehub: Dashboard → Project Settings → ProductUID, or see [Finding a ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid).)
-
-3. **Via Arduino IDE:** Select Tools → Board → `Arduino Opta RS485` (or WiFi), select the correct COM/tty port, then Sketch → Upload.
-
-4. **Via command line (arduino-cli):**
- ```bash
- arduino-cli compile --fqbn arduino:mbed_opta:opta_rs485 firmware/vfd_pump_monitor/
- arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:mbed_opta:opta_rs485 firmware/vfd_pump_monitor/
- ```
- (Replace `/dev/ttyACM0` with your OPTA's serial port; on macOS it may be `/dev/cu.usbmodem*` or similar.)
-
-5. Open the Arduino IDE Serial Monitor (115200 baud) to verify the sketch boots and reports "Notecard configuration complete."
-
-## 9. Data Flow
-
-
-
-**Collected.** Every `sample_minutes`: output frequency, motor current, output torque, drive heatsink temperature, cumulative runtime hours, **active** fault code (not the fault history log, the live fault register only).
-
-**Summarized.** Every `report_minutes` (default hourly): mean and peak of each scalar, separate run/stop minute counts, total runtime hours, count of distinct fault *transitions* observed in the hour, and the last non-zero fault code seen.
-
-**Transmitted.**
-- `vfd_summary.qo` — once per `report_minutes` (default 24 Notes per day), queued and shipped by the Notecard's hourly outbound sync.
-- `vfd_event.qo` — immediately on rule trigger, with `sync:true` to bypass the outbound interval.
-
-**Routed.** Notehub fans `vfd_event.qo` out to whatever real-time channel the operator uses (CMMS ticket creation, on-call paging, Slack, etc.) and `vfd_summary.qo` to a long-term store for trend analysis.
-
-**Triggers.** Four rules fire alerts:
-- `load_anomaly` — hourly mean current exceeds the rolling baseline by `current_alarm_factor` while running. *Many* root causes can drive this (bearing drag, fouling, valve position, viscosity, debris, some impeller conditions); the alert flags an anomaly worth investigating, not a specific failure mode.
-- `transient_faults` — `transient_fault_count` or more distinct fault *transitions* (count of `0 → nonzero` or `code-changed` events, not count of samples while a fault is asserted) within `transient_fault_window_hours`.
-- `runtime_drift` — observed daily runtime exceeds `expected_run_hours_per_day` by more than 25%.
-- `drive_overtemp` — heatsink temperature exceeds `drive_temp_alarm_c`.
-
-## 10. Validation and Testing
-
-Expected steady-state behavior on a healthy pump: one summary Note per hour and zero event Notes. The Notecard's [`card.status`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-status) and [`hub.status`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-status) requests are useful smoke tests during commissioning.
-
-**Modbus first-light.** Before connecting to the real drive, run the firmware against a [USB-to-RS-485 adapter](https://www.sparkfun.com/products/9822) and a software Modbus simulator (Modbus Mechanic, ModRSsim2, or equivalent) wired to the OPTA's RS-485 terminals. Verify the six register reads match what the simulator is publishing.
-
-**Power validation with Mojo.** The Notecard's published current envelope (from the [Notecard low-power-design docs](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) and the [NOTE-WBNAW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-wbnaw/)):
-
-| Phase | Expected current |
-|---|---|
-| Notecard idle (radio off, between syncs) | ~8–18 µA @ 5V |
-| Modem active (cellular session) | ~250 mA average, with ≤2 A bursts during GSM transmit |
-| WiFi active (when WiFi fallback engaged) | ~80 mA average |
-
-Splice the [Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) inline on the Wireless for OPTA power input and confirm: (a) idle current is in the published µA range between syncs, (b) per-session energy lands in the few-mAh range for an hourly outbound sync, and (c) total energy per day is consistent across runs. Note that this measurement is the **whole expansion subsystem** — Notecard plus the expansion's onboard regulators and I²C glue, not the Notecard alone, unless you physically isolate the Notecard's `VMODEM_P` rail. Also Note that the Notecard's lowest-power state requires `VUSB` not present and `AUX_EN` not held high; see the [Notecard low-power-design docs](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for the gating conditions.
-
-**Fault simulation.** Easiest path: drop `current_alarm_factor` to 1.0 in the Fleet's environment variables — the next `inbound` sync will pull the new value, the next hourly summary will trip `load_anomaly`, and the event will land in Notehub within a session-establishment window.
-
-## 11. Troubleshooting
-
-**Notecard not claiming to the project.**
-- Verify `PRODUCT_UID` in the sketch exactly matches the ProductUID on Notehub (Projects → Project Settings).
-- Confirm cellular signal: in the Blues In-Browser Terminal, run `card.status` and check the `"signal"` field. Minimum -100 dBm for LTE Cat-1.
-- If deploying indoors or in a metal cabinet without an external antenna, the bundled rubber-duck antenna will not work — thread the external SMA antenna through a cable gland and screw it to the primary antenna port.
-
-**Modbus reads failing (firmware logs "Modbus error" or no data in summary Notes).**
-- Confirm RS-485 A/B/COM wiring is correct. A and B are easy to swap; the drive will not respond if they're reversed.
-- Verify 120 Ω termination resistor is present at **both ends** of the bus. With one OPTA and one drive, that means two resistors total (one at the OPTA, one at the drive).
-- Check slave ID and baud rate: in Notehub's Fleet Environment, confirm `modbus_slave_id` and `modbus_baud` match the VFD's configuration. Run `card.status` in the terminal after setting the environment variables — it shows inbound sync time and confirms the device pulled the new values.
-- Before connecting to the real drive, test with a [USB-to-RS-485 adapter](https://www.sparkfun.com/products/9822) and a software Modbus simulator (Modbus Mechanic, ModRSsim2) wired to the OPTA's RS-485 terminals. Verify the firmware reads all six registers correctly.
-
-**First event not appearing in Notehub after 60+ seconds.**
-- Check Notehub's Events tab; all Notes (summary and alert) appear there. If empty, the firmware has not yet established a cellular session — wait for the first outbound sync (default 60 minutes) or power-cycle the OPTA to force a session sooner.
-- Confirm the Notecard has cellular coverage (see above: `card.status` signal field).
-- If you need an alert to appear immediately for testing, drop `current_alarm_factor` to 1.0 in the Fleet Environment — the next hourly summary will trigger `load_anomaly`. The alert event will post within a session-establishment window (typically 15–60 seconds after the inbound sync pulls the new threshold).
-
-**Environment variables not taking effect.**
-- Environment variables are fetched at the inbound sync interval (default 120 minutes). To force an immediate fetch, power-cycle the OPTA or reduce `inbound` in Notehub to 1 minute for commissioning.
-- Confirm the variables are set in the correct Fleet, not the project level. In Notehub: Projects → Project Name → Fleets → Fleet Name → Environment.
-
-**Antenna placement considerations.**
-- Rubber-duck antennas supplied with the Wireless for OPTA are for bench testing only. In a metal pump cabinet, they will not maintain reliable LTE Cat-1 coverage.
-- Route the primary antenna outside the cabinet through a cable gland. If the layout allows, also route the diversity antenna (second SMA port) for improved performance in marginal-signal areas.
-- Keep the primary antenna at least 2 m away from high-power electrical equipment (motor starters, VFDs) to reduce RF interference.
-
-## 12. Limitations and Next Steps
-
-A reference design that drops into thousands of different drive configurations has to draw a sharp line around what it does and doesn't claim. The biggest line is around what VFD telemetry can actually tell you; the rest is the usual list of scope choices that get refined when a real fleet starts deploying.
-
-**Signal limitations.** VFD telemetry is valuable but it is not equivalent to full pump instrumentation. Current, torque, speed, temperature, and active fault codes can identify abnormal *patterns*, but they cannot conclusively distinguish between worn impeller, clogged suction strainer, closed discharge valve, cavitation, bearing drag, increased fluid viscosity, debris, or process changes — without supporting context such as suction/discharge pressure, flow, vibration, or known duty cycle. Treat every alert this firmware emits as an *early maintenance indicator*, not a diagnosis.
-
-### Simplified for this reference design
-
-Beyond the signal limitation above, the firmware draws a deliberate scope boundary in a few places. Each of these is a place a real fleet deployment will extend the design rather than a defect.
-
-**Vendor-specific register addresses, scaling, signedness, and word counts.** Defaults are illustrative for a fictional contiguous map. Each VFD vendor publishes its own Modbus map, so commissioning a real plant means looking up the actual addresses, scaling factors (current may be 0.1 A, 0.01 A, or % of rated), signedness (torque and temperature are often signed), word counts (runtime hours are often a 32-bit value across two registers with vendor-specific word order), and addressing convention (0-based wire-level vs. 1-based / Modicon "40001" notation). The shipped firmware reads six contiguous 16-bit registers with hardcoded scaling — **production builds need vendor-specific firmware, one build per `vfd_profile`.**
-
-**Active fault code only, not fault history log.** The firmware reads the active-fault register once per cycle. A vendor-specific fault-log readout — typically a multi-register block with circular-buffer semantics — is a future enhancement.
-
-**Single drive per OPTA.** The firmware reads one slave ID. A pump room with N drives needs either N OPTAs or a firmware extension to round-robin across slave IDs on the same RS-485 bus.
-
-**Heuristic thresholds.** Four threshold-based rules catch common load-anomaly, transient-fault, runtime-drift, and overtemp patterns. They will *not* catch every failure mode, and they will sometimes fire on benign conditions such as a seasonally hotter ambient or a deliberate process change. Production deployments should commission thresholds per pump after a baseline period.
-
-**No Modbus writes.** The firmware reads only. Writing setpoints to the drive (start/stop, speed reference, and the like) is intentionally out of scope — that's a different safety conversation involving E-stop wiring, lockout/tagout, and functional safety certification.
-
-**No host firmware updates wired up.** [Notecard Outboard Firmware Update](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) is supported on STM32H7 (the OPTA's MCU family) but requires AUX wiring that Blues Wireless for OPTA does not currently break out. For now, **host firmware updates are local-only via USB-C.**
-
-### Production Next Steps
-
-When a real fleet starts deploying, the scope choices above become the roadmap — roughly in order from the most immediately useful to the most infrastructure-dependent.
-
-**Vendor-specific `vfd_profile` builds** are the first need: ABB ACS580, Yaskawa GA500, Danfoss FC-302, Schneider ATV — each with the correct register addresses, scaling, signedness, and word handling baked in.
-
-**Per-pump baseline learning** sharpens detection considerably. Storing rolling 30-day current-vs-frequency tables in flash lets the firmware trigger `load_anomaly` against the *learned* curve rather than a static factor.
-
-**Fault history log readout** — vendor-specific and typically multi-register with timestamps — captures the events leading up to a trip, not just the active fault.
-
-**Multi-drive support** via round-robin polling and per-slave-ID summaries lets a single OPTA service an entire pump room.
-
-**A `vfd_command.qi` inbound Notefile** would allow service-tech-initiated diagnostic dumps on demand.
-
-**Wiring ODFU to the OPTA's BOOT/RESET pins** is the longest-horizon item, enabling over-the-air host updates across the fleet once the hardware path is available.
-
-## 13. Summary
-
-The maintenance team that used to learn about a struggling pump from a dry tank on a Saturday night now gets a CMMS ticket on Tuesday afternoon — a load anomaly, a creeping drive temperature, a cluster of transient faults. Six Modbus registers, one cellular session an hour, and four anomaly rules turn the VFD's existing telemetry into a stream of leading indicators, on a device a plant electrician can clip onto a DIN rail in fifteen minutes without ever touching the corporate network.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/vfd-pump-predictive-maintenance/)**
diff --git a/52-vfd-pump-predictive-maintenance/banner.png b/52-vfd-pump-predictive-maintenance/banner.png
deleted file mode 100644
index 46fbc290..00000000
Binary files a/52-vfd-pump-predictive-maintenance/banner.png and /dev/null differ
diff --git a/52-vfd-pump-predictive-maintenance/diagrams/01-system-architecture.svg b/52-vfd-pump-predictive-maintenance/diagrams/01-system-architecture.svg
deleted file mode 100644
index b88c3bda..00000000
--- a/52-vfd-pump-predictive-maintenance/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,65 +0,0 @@
-
diff --git a/52-vfd-pump-predictive-maintenance/diagrams/02-wiring-assembly.svg b/52-vfd-pump-predictive-maintenance/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 7ca421d5..00000000
--- a/52-vfd-pump-predictive-maintenance/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,83 +0,0 @@
-
diff --git a/52-vfd-pump-predictive-maintenance/diagrams/03-data-flow.svg b/52-vfd-pump-predictive-maintenance/diagrams/03-data-flow.svg
deleted file mode 100644
index 33b5557c..00000000
--- a/52-vfd-pump-predictive-maintenance/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/53-municipal-wastewater-lift-station-monitor/README.md b/53-municipal-wastewater-lift-station-monitor/README.md
index b01414ac..d26a7fc3 100644
--- a/53-municipal-wastewater-lift-station-monitor/README.md
+++ b/53-municipal-wastewater-lift-station-monitor/README.md
@@ -1,512 +1,5 @@
# Municipal Wastewater Lift Station Monitor
-
+A cellular or satellite retrofit for municipal wastewater lift stations that catches pump failures, discharge obstructions, and high-water conditions before they become a sanitary overflow.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Municipal%20Wastewater%20Lift%20Station%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/lift_station_monitor/](firmware/lift_station_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [downtime prevention](https://blues.com/downtime-prevention/) retrofit for municipal wastewater lift stations that catches pump failures, discharge obstructions, and high-water conditions before they become a sanitary overflow. A handful of sensors and a single [Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) — one module that carries cellular, WiFi, and Skylo satellite radios and fails over between them automatically — transform a sealed concrete vault into a remotely-monitored station that delivers alerts to the on-call crew within minutes, not hours after a manual site visit. One SKU and one firmware image cover the entire fleet, from the station three blocks from city hall to the one at the edge of the service territory beyond any cell tower.
-
-## 1. Project Overview
-
-**The problem.** A **lift station** (also called a pump station) is a below-grade concrete vault or roadside cabinet that collects raw sewage from the surrounding gravity sewer system and pumps it uphill toward the treatment plant. Every municipality has dozens of them, often scattered across low-lying neighborhoods, industrial zones, and rural road shoulders — most with no onsite staff and no way to know what's happening inside until a citizen calls to report a smell or, worse, a spill.
-
-When a lift station fails, the **wet well** — the collection basin that feeds the pumps — fills up and overflows. The result is an **SSO**: a sanitary sewer overflow. SSOs draw immediate regulatory attention; they trigger EPA reporting obligations, risk consent decree violations, and require expensive emergency cleanups. A station that fails on a Friday evening and isn't discovered until Monday morning is a public health event, a PR crisis, and a significant unplanned expense all at once. The failure modes are rarely dramatic: a pump fails to start because its float control sticks, a discharge check valve fails and allows backflow that clogs the impeller, or a wet-well float switch trips but no one receives the alarm because the SCADA dial-up modem lost its phone line. Each of these is detectable minutes after it starts — if someone is watching.
-
-This project is that watcher. It straps to the inside of the station, samples the wet-well level every 60 seconds, measures current draw on each pump, and monitors the high-water float switch — four sensing points across three sensor types. Onboard edge logic on the STM32-based host MCU evaluates three fault rules every 60 seconds and routes alerts to the [Blues Notehub](https://blues.com/notehub/) cloud service the instant any rule trips — over cellular where a tower is in reach, and over the Skylo satellite network where one isn't. The on-call crew gets paged before the wet well overflows, not after.
-
-**Why Notecard for Skylo.** The wireless-first architecture here isn't a convenience — it's a necessity. Lift stations sit in concrete vaults underground, often with no AC power outlet in the vault itself (power runs to the pump control panel, not a wall socket). They're geographically distributed across a municipality in a pattern that matches the sewer network, not the municipal network: there's no fiber running to a roadside pump cabinet, and there's no corporate WiFi AP that can reach through a concrete lid to a sensor inside. Utility supervisors would need to deploy and maintain a WiFi access point at every single station to achieve what one Notecard covers automatically.
-
-[Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) (NOTE-NBGLWX) carries three radios on one M.2 module — cellular (LTE-M / NB-IoT / GPRS), WiFi, and satellite over the [Skylo](https://www.skylo.tech/) non-terrestrial network (NTN) — and selects among them automatically. The firmware sets a single [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) preference of `wifi-cell-ntn`: prefer WiFi where an accessible AP happens to be in range (rare for a sealed vault), fall back to cellular (the de-facto primary at the vast majority of municipal infrastructure), and fall back again to Skylo satellite at stations on the fringe of the service territory, beyond the reach of any cellular carrier. Failover is handled inside the Notecard; the host firmware never branches on which network is live. That collapses what used to be a two-SKU decision — a cellular Notecard for in-coverage stations, a satellite device for the rest — into a single part number, a single antenna kit, and a single firmware image that deploys unchanged across the entire fleet. There is nothing to swap when a station turns out to have weaker coverage than the survey suggested: the same board that runs on cellular downtown automatically reaches the Skylo network at the rural lift station.
-
-
-
-**Deployment scenario.** A sealed NEMA 4X enclosure mounted **inside the lift station's above-grade control cabinet**, powered from the 120 VAC control circuit that already powers the pump starters. The specified Blues hardware and NEMA 4X ABS enclosure are **not** rated for hazardous (classified) locations. Wet wells and sealed vaults can accumulate methane and hydrogen sulfide — both potentially classified atmospheres under NFPA 820 / NEC Article 820. Do not install this hardware inside the wet well or any classified-atmosphere zone. If your jurisdiction classifies the vault interior as a hazardous location, any hardware in that zone must be rated for the classification; consult a licensed electrical engineer before proceeding. Sensor cables enter through conduit fittings: one multiconductor cable to the submersible level transducer in the wet well, two split-core CT jaws clamped around the pump motor supply conductors inside the control panel, and one float switch cable to a new dedicated high-water alarm float switch hung in the wet well alongside the station's existing level floats. No station modification is required beyond adding three sensor connections to the existing control wiring. Notecard for Skylo's antenna cables exit through a conduit fitting to outdoor-mounted antennas on the cabinet exterior or above-grade access point — the same Skylo-certified antenna carries both cellular and satellite, so no antenna swap is needed if a station ends up relying on the satellite link.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Inside the above-grade cabinet, the Cygnet STM32L433 host on the Notecarrier CX wakes once a minute via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn), reads three sensor types across four inputs (level, CT1, CT2, float switch), and walks the three fault rules before the wet well can shift more than an inch or two. The Notecard sits next to it on I²C — no AT commands, no modem state machine, no serial buffers to babysit. Anything that has to survive the next 60-second power-cut (the previous level reading, alert cooldowns, summary accumulators) gets persisted into the Notecard's flash via `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep`, so the host can lose power between samples and pick up exactly where it left off.
-
-**Notecard responsibilities.** Notecard for Skylo runs the same playbook regardless of which radio is live: queue [Notes](https://dev.blues.io/api-reference/glossary/#note) locally, ship them on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes), and push any `sync:true` alert to the head of the queue for the next available session. The `card.transport` `wifi-cell-ntn` preference set at boot decides the path automatically: on cellular that means a Note in flight 15–60 seconds after the host queues it; when the unit has fallen back to satellite it means first-in-line for the next Skylo session, which takes a few minutes rather than seconds. The same Notecard also distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from Notehub on every inbound sync, so a utility supervisor can retune level setpoints, current thresholds, or rising-rate sensitivity for the whole fleet from a browser — no firmware reflash, no vault lid lifted.
-
-**Notehub responsibilities.** The Notecard's embedded global SIM and bundled Skylo satellite allocation hand events off to [Notehub](https://notehub.io), which timestamps and stores every one and applies project-level [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). Alerts and summaries land in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) by design — `lift_alert.qo` can fan out to PagerDuty or SMS while `lift_summary.qo` lands in a long-term analytics store, with no filter logic in the route itself. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) make it easy to group stations by service zone or pump type while still allowing per-station overrides for the one vault that always reads a little funny.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and a wide range of other destinations; route setup is project-specific and not implemented here. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for configuration guidance.
-
-### Satellite (NTN) operation considerations
-
-The same Notecard for Skylo runs the same firmware everywhere, but at stations that routinely fall back to the satellite link the NTN path has operational characteristics that must be planned for, not assumed away. These apply only while the unit is transmitting over satellite — stations within cellular coverage behave like any other cellular Notecard.
-
-**Alert latency.** `sync:true` Notes are prioritized for the next available satellite session, but locating a Skylo satellite and completing transmission can take several minutes. When a station is operating over satellite, "alert in minutes" is realistic; "alert in seconds" is not. The 30-minute alert cooldown in the firmware is still meaningful because a satellite station is reporting a fault to the crew before the wet well overflows, not instantaneously.
-
-**Inbound sync cadence and data cost.** Each inbound sync (used to pull updated environment variables from Notehub) consumes approximately 50 bytes of satellite data. At the default `inbound:120` (every 2 hours), that is ~600 bytes per day — a significant fraction of the bundled 10 KB satellite allocation. For stations expected to operate over satellite, set `inbound_interval_min` to `240` or higher via the Notehub environment-variable panel to reduce inbound satellite data consumption. The firmware re-issues `hub.set` whenever `inbound_interval_min` or `summary_interval_min` changes, so neither adjustment requires a firmware reflash (see the env-var table in [Section 5](#6-notehub-setup)).
-
-**Payload discipline.** The Skylo NTN link enforces a hard 256-byte maximum per Note; Notes exceeding this limit are silently dropped by the satellite network. The [`note.template`](https://dev.blues.io/api-reference/notecard-api/note-requests/#note-template) encoding used by this firmware (with `format:"compact"` and a numeric `port`) keeps both `lift_alert.qo` and `lift_summary.qo` well within that ceiling. Do not add free-form string fields to these Notefiles, and validate payload size on any schema change.
-
-**Antenna placement.** Satellite operation requires Notecard for Skylo's main antenna outdoors and free from obstructions — for the northern hemisphere, an unobstructed view of the southern sky, where Skylo's GEO satellites sit above the equator. A station where the enclosure is entirely below grade or inside a steel cabinet will require an above-grade antenna cable run; plan that conduit path at installation time. Use only the Skylo-certified antenna supplied with Notecard for Skylo on the `MAIN` u.FL port; substituting an uncertified antenna risks regulatory non-compliance and link failure. This same antenna also carries the terrestrial cellular signal, so the placement that enables satellite fallback serves cellular as well.
-
-**Power envelope.** Notecard for Skylo idles at typically ~8 µA. A network session — cellular or satellite — draws on the order of ~250 mA average from the onboard modem, with brief higher peaks (the BG95-S5 can pull nearly 2 A for a few milliseconds on a 2G transmit burst). The HDR-15-5 (3 A rated) handles these peaks with margin. See the [Validation section](#9-validation-and-testing) for a per-state current breakdown.
-
-**Mandatory initial non-NTN sync.** Before any satellite (NTN) operation is possible, Notecard for Skylo must complete at least one non-NTN sync with Notehub over cellular or WiFi to associate with the project and register Notefile templates. The cold-boot `hub.set` in `periodic` mode performs that first sync over cellular/WiFi automatically, so ensure the unit has cellular (or WiFi) coverage during initial commissioning, even if the deployment site relies on satellite for routine operation.
-
-## 3. Technical Summary
-
-**What you'll have:** A [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) + Notecard for Skylo sending sample lift_alert and lift_summary events to your Notehub project every 60 seconds without needing sensors in the field.
-
-1. **Create a Notehub project** at [notehub.io](https://notehub.io) and copy its ProductUID.
-2. **Flash the firmware:**
- ```bash
- arduino-cli board install stm32duino:STM32:1.11.0
- arduino-cli lib install "Blues Wireless Notecard"
- cd firmware/lift_station_monitor
- arduino-cli compile --fqbn stm32duino:STM32:Notecarrier_CX \
- --build-property "compiler.cpp.extra_flags=-DPRODUCT_UID=\"com.example:demo\"" \
- -u -p /dev/ttyACM0
- ```
- (Replace `/dev/ttyACM0` with your Notecarrier serial port; on macOS use `/dev/tty.usbmodem*`; on Windows use `COM*`.)
-3. **Open serial monitor** at 115200 baud; verify logs show "Notecard configured," `hub.set` requests, and `note.add` calls.
-4. **Check Notehub Devices** — your Notecard appears within 60 seconds. Click it to see `lift_alert.qo` and `lift_summary.qo` events in the Events panel.
-5. **Tune thresholds** — in the Fleet panel, set environment variables (e.g., `high_level_pct: 50.0`) and watch the serial log show the updated values on the next wake.
-
-For bench-only testing, use compile-time flags to inject synthetic sensor values (see "Bench fault simulation" below in Section 8).
-
-A single SKU covers the full deployment spectrum. Every station uses a **[Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)** ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)): stations within LTE coverage transmit over cellular, and stations beyond reliable cellular reach fall back automatically to the Skylo satellite network — no second device, no different part number, no antenna swap. The firmware and Notehub configuration are identical everywhere; stations expected to operate over satellite simply benefit from wider inbound sync intervals to conserve bundled satellite data. See [Satellite (NTN) operation considerations](#satellite-ntn-operation-considerations) in Section 2.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "lift_alert.qo",
- "body": {
- "alert": "pump_fail_to_start",
- "level_pct": 87.4,
- "pump1_amps": 0.2,
- "pump2_amps": 0.1,
- "float_sw": false
- },
- "sync": true
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host — no separate MCU needed for this analog + digital sensor mix. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)) | 1 | One M.2 module carrying cellular (LTE-M / NB-IoT / GPRS, Quectel BG95-S5 modem), WiFi (Silicon Labs WFM200S), and Skylo satellite (NTN) radios. The firmware's `card.transport` `wifi-cell-ntn` setting makes it prefer cellular at in-coverage stations and fall back automatically to the Skylo satellite network at stations beyond cellular reach — no second device or part-number decision. Removes per-site network provisioning; the embedded global SIM and bundled satellite allocation cover the whole fleet from one SKU. Requires the antennas below. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb-counter on the power rail for bench-validation of the sleep/wake/transmit energy profile. Not deployed to the field. |
-| [WIKA LH-10](https://www.wika.com/en-us/lh_10.WIKA), 0–15 PSI gauge, 4–20 mA 2-wire, 316L SS, IP68, with vented cable | 1 | Submersible hydrostatic level transmitter purpose-built for water/wastewater wet-well immersion (the WIKA S-10 is a general-purpose / sanitary transmitter; the LH-10 is the family member rated for permanent submersion in raw sewage). The vented polyurethane cable (an internal vent tube equalizes the sensor's reference side to atmosphere, making the measurement gauge pressure = water head) doubles as the support tether — secured with a stainless cable grip at the cover plate. 316L SS wetted parts tolerate raw sewage. 15 PSI range covers ≈ 10 m of wet-well head. 4–20 mA 2-wire loop-powered output; wiring is identical to any other loop-powered transmitter. Specify "vented cable" and M20×1.5 or ½ NPT conduit seal fitting when ordering. Available from WIKA, instrumart.com, and major industrial distributors. |
-| 150 Ω, 1% resistor (level sensor shunt) | 1 | Converts 4–20 mA loop current to 0.6–3.0 V for the 3.3 V STM32 ADC (full-scale within VREF). |
-| [SCT-013-030 split-core CT, 30 A / 1 V RMS](https://www.sparkfun.com/products/11005) (SparkFun SEN-11005) (current-output CT; add external burden resistor for 1 V_rms scaling) | 2 | Non-invasive current sensing on each pump motor supply lead; no break in the power circuit required. 30 A range is appropriate for single-phase motors up to approximately 5 HP at 230 V (FLA ≈ 28 A). Larger or three-phase motors require a higher-ratio CT. See Limitations. |
-| [TRRS 3.5 mm audio jack breakout](https://www.sparkfun.com/products/11570) (SparkFun BOB-11570) | 2 | The SCT-013's pigtail terminates in a TRRS plug; this breakout exposes Tip and Sleeve for the AC signal and shield/return. |
-| 10 kΩ, 1% resistor (CT bias divider, 2 per pump) | 4 | Two-resistor divider centers the AC CT signal at VREF/2 ≈ 1.65 V so the unipolar STM32 ADC sees only positive voltages. |
-| 10 µF electrolytic capacitor (CT bias decoupling, 1 per pump) | 2 | Low-pass filter on the bias node; reduces HF noise on the ADC input. |
-| Gems Sensors RS-500-Y-PP, SPST N.O., polypropylene float switch | 1 | Sewage-rated polypropylene construction; normally-open contact closes on high-water. Mounts through the wet-well cover or on a cable-held hanger bracket. Available from Grainger and industrial distributors. Specify vertical or horizontal actuation to match the wet-well geometry. |
-| [MeanWell HDR-15-24](https://www.meanwell.com/Upload/PDF/HDR-15/HDR-15-SPEC.PDF), 85–264 VAC input, 24 VDC / 0.63 A, DIN-rail | 1 | AC–DC DIN-rail supply that derives 24 VDC from the station's 120 VAC control circuit. Powers the 4–20 mA sensor loop; 15 W is ample for the 0.48 W peak loop load. |
-| [MeanWell HDR-15-5](https://www.meanwell.com/Upload/PDF/HDR-15/HDR-15-SPEC.PDF), 85–264 VAC input, 5 VDC / 3 A, DIN-rail | 1 | AC–DC DIN-rail supply that derives 5 VDC for the Notecarrier CX USB-C port from the same 120 VAC control leg. |
-| Skylo-certified LTE/satellite antenna included with Notecard for Skylo (u.FL, supports the S-Band / L-Band B23 / B255 / B256 bands) | 1 | Connects to the `MAIN` u.FL port and carries **both** the terrestrial cellular signal and the Skylo satellite link — a single antenna for both networks. Use only the Skylo-certified antenna supplied with Notecard for Skylo; substituting an uncertified antenna risks regulatory non-compliance and link failure. Mount outdoors on the cabinet exterior or above-grade access point with an unobstructed view of the sky (northern hemisphere: the southern sky); route through a liquid-tight fitting. For an external SMA mag-mount instead of the bare u.FL antenna, add a u.FL-to-SMA-F bulkhead pigtail (e.g. Taoglas CAB.0150.A.01). |
-| Passive GPS/GNSS antenna (u.FL) per the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | 1 | Connects to the `GPS` u.FL port for GNSS time/location. Mount outdoors with a clear sky view alongside the main antenna; route through a liquid-tight fitting. |
-| Hammond 1554N2GCLY NEMA 4X ABS enclosure, 8.07 × 6.10 × 3.94″ | 1 | Polycarbonate-gasketed splash-resistant housing. Use liquid-tight conduit fittings (Heyco or equivalent) for all sensor cable entries. |
-
-**Bundled connectivity (Notecard for Skylo):** Ships with an active global SIM including 500 MB of cellular data and 10 years of service, **plus** 10 KB of bundled Skylo satellite data — no activation fees, no monthly commitment, and no separate satellite provider subscription. Additional satellite data is billed per byte (see the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for current pricing). Minimizing inbound sync frequency conserves the bundled satellite allocation. See [Section 2](#satellite-ntn-operation-considerations) for guidance.
-
-## 5. Wiring and Assembly
-
-
-
-
-
-**Safety.** Lift-station control panels contain 120 VAC mains wiring operating in wet, corrosive, and potentially hazardous-atmosphere environments. Installation must be performed by a **licensed electrician** or qualified instrumentation technician following your jurisdiction's electrical code and your utility's **lockout/tagout (LOTO)** procedures before opening any panel. Wet wells are classified confined spaces — follow applicable confined-space entry (CSE) regulations (OSHA 29 CFR 1910.146 in the US) before entering the vault: test for H₂S and oxygen deficiency, establish an attendant and rescue plan, and use supplied-air or appropriate respiratory protection as required. Raw sewage contains pathogens; wear appropriate PPE and observe hygiene protocols for all work inside the vault. This reference design is **sensor and monitoring only** — it does not command pump start/stop and makes no safety-critical outputs.
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin headers. Notecard for Skylo seats into the M.2 slot. Its `MAIN` u.FL port connects to the included Skylo-certified antenna, which carries both the cellular and satellite signals — connect it directly, or route it through a u.FL-to-SMA-F bulkhead pigtail in the enclosure wall if you are using an external SMA mag-mount. Its `GPS` u.FL port connects to the passive GPS/GNSS antenna. Both antennas mount outdoors with a clear sky view (northern hemisphere: the southern sky), so the unit can reach the Skylo satellite network wherever it falls back from cellular. The Mojo connects via the [Qwiic](https://www.sparkfun.com/qwiic) connector on the Notecarrier and sits inline between the 5 V supply and the Notecarrier's +VBAT pad during bench validation.
-
-**Level sensor (4–20 mA current loop)**
-
-The WIKA LH-10 is a submersible hydrostatic transmitter purpose-built for wet-well immersion, with a 4–20 mA two-wire loop-powered output and 316L stainless wetted parts. (The S-10 referenced in some WIKA literature is the family's general-purpose / sanitary variant, do not substitute it for permanent submersion in raw sewage.)
-
-**Physical installation.** Lower the sensor into the wet well to a stable depth above the lowest feasible pumped level. The vented cable is the support tether — secure it with a stainless cable grip or strain-relief clamp at the wet-well cover plate so the sensor's weight is carried mechanically, not by the conductors. The vent tube inside the cable must be kept clear and open to atmosphere at the enclosure end; do not seal or submerge the vent opening. Thread the two conductors through a liquid-tight conduit fitting into the enclosure.
-
-**Loop wiring:**
-- `+24 V` from the MeanWell HDR-15-24 → sensor `+` conductor (typically red or brown, confirm with the cable marking).
-- Sensor `–` conductor (black or blue) → 150 Ω, 1% shunt resistor → `GND`.
-- Notecarrier CX `A0` → junction between the sensor `–` conductor and the **top** of the 150 Ω shunt.
-
-The 150 Ω shunt converts the 4–20 mA loop current to 0.6–3.0 V, which maps linearly to 0–100% wet-well depth within the STM32L433's 3.3 V ADC reference. Loop margin: `V_supply – V_shunt_max = 24 V – 3.0 V = 21 V`, well above the sensor's minimum loop voltage (typically 10–12 V).
-
-**Pump current sensors (SCT-013-030 CTs)**
-
-Each CT clips around one supply conductor feeding a pump motor (clamp on only one leg, clamping both legs will cancel the fields and read zero). Wire each CT circuit identically; the following shows pump 1 on `A1`:
-
-- Insert the CT's TRRS plug into the SparkFun BOB-11570 breakout.
-- Tip (signal) and Sleeve (return/shield) are the active terminals on the BOB.
-- Bias circuit on `A1`: connect two 10 kΩ resistors in series from `+3V3` to `GND`; the junction (midpoint) is the bias node at VREF/2 ≈ 1.65 V. Place a 10 µF capacitor from the bias node to `GND` to filter HF noise.
-- CT Tip → bias node; CT Sleeve → `GND`.
-- Notecarrier CX `A1` → bias node.
-
-Repeat for pump 2 CT on `A2` with its own independent bias circuit.
-
-**Float switch**
-
-Install the Gems RS-500-Y-PP as a **dedicated monitoring float**, independent of the station's existing pump-control and SCADA float switches. Do not tap into existing float-control or alarm wiring — those circuits typically carry 120 VAC or drive pump starter contactors and are not safe or appropriate for direct GPIO connection. Hang the new float at the desired high-water monitoring setpoint (typically just below the station's rated overflow level) alongside the station's existing level floats; the wet-well cover usually has spare conduit entries or room for an additional cable grip.
-
-The Gems RS-500-Y-PP provides a galvanically isolated SPST N.O. dry contact rated for this environment:
-- One float switch terminal → Notecarrier CX `D2`.
-- Other terminal → `GND`.
-- In firmware, `D2` is configured `INPUT_PULLUP`; the switch closing pulls the pin to GND (logic LOW = alarm active).
-
-**Power**
-
-- 120 VAC from the station's control circuit → MeanWell HDR-15-24 (24 VDC) and MeanWell HDR-15-5 (5 VDC), both DIN-rail mounted inside the NEMA 4X enclosure.
-- HDR-15-24 `V+` → 4–20 mA level sensor loop positive terminal.
-- HDR-15-24 `V-` → system GND (the same node as Notecarrier CX GND and the bottom of the 150 Ω shunt). This common connection is required to complete the 4–20 mA loop return path. Without it the loop current has no return and the shunt voltage will be incorrect.
-- HDR-15-5 `V+` → Notecarrier CX USB-C port (or +VBAT pad if USB-C is not used).
-- HDR-15-5 `V-` → system GND.
-- Mojo sits inline on the 5 V rail between the HDR-15-5 output and the Notecarrier CX power input during bench validation (see Section 8).
-
-## 6. Notehub Setup
-
-### 6.1 Project and Device Claim
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into the firmware as `PRODUCT_UID` (in `lift_station_monitor_helpers.h`). Rebuild and flash.
-2. **Claim the Notecard.** Power the Notecarrier CX. On first cellular sync, the Notecard associates with your project automatically — no manual claim step required. Watch the **Devices** panel in Notehub; your Notecard appears within 60 seconds.
-
-### 6.2 Environment Variables for Threshold Tuning
-
-All thresholds below are optional overrides of firmware defaults. Set them via **Notehub > Fleet > Environment**, not in the firmware. Any variable set in Notehub is picked up by the device on its next inbound sync without a firmware reflash.
-
-| Variable | Default | Purpose |
-|---|---|---|
-| `pump_on_amps` | `3.0` | Current draw (A) above which a pump is considered running. Adjust for larger/smaller motors. |
-| `high_level_pct` | `85.0` | Wet-well fill level (%) at which the fail-to-start check activates. Should be set below the float switch trip point. |
-| `rising_rate_pct` | `2.0` | Level rise (% per 60-second sample) while a pump is running that triggers a clog alert. |
-| `summary_interval_min` | `60` | Minutes between summary Notes. The firmware also re-issues `hub.set outbound` to match this value whenever it changes, keeping the Notecard's outbound sync window aligned with the summary rate. |
-| `inbound_interval_min` | `120` | Minutes between inbound syncs (environment-variable pulls from Notehub). **Satellite deployments:** set to `240` or higher to conserve bundled satellite data (~50 bytes per inbound sync). The firmware re-issues `hub.set inbound` to match this value whenever it changes — no firmware reflash required. |
-
-To set variables: Click your project's **Fleet**, then the **Environment** tab. Add each variable as a key-value pair (e.g., `high_level_pct = 75.0`), then click **Save**. The device pulls the updated values on its next inbound sync.
-
-### 6.3 Routing Events to the Outside World
-
-Add [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) in **Notehub > Routes** to forward events to your on-call notification system (PagerDuty, Slack, email, webhook) or analytics backend:
-
-- **Alert route:** Route `lift_alert.qo` to your real-time on-call service (Notehub supports HTTP, MQTT, AWS SNS, Azure Event Hubs, Slack, PagerDuty, and others).
-- **Summary route:** Route `lift_summary.qo` to a long-term analytics store (Snowflake, BigQuery, TimescaleDB, etc. via HTTPS or JDBC).
-
-Keeping alerts and summaries in separate Notefiles means each route handles them independently — alerts fire immediately for urgent notification, summaries batch for efficient storage. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for supported destination types and step-by-step setup.
-
-## 7. Firmware Design
-
-The firmware is split across three files in [`firmware/lift_station_monitor/`](firmware/lift_station_monitor/):
-
-| File | Role |
-|------|------|
-| [`lift_station_monitor.ino`](firmware/lift_station_monitor/lift_station_monitor.ino) | Main sketch: `setup()`, `loop()`, `runSampleCycle()`, `runDetectionCycle()`, `sendAlert()`, `sendSummary()` |
-| [`lift_station_monitor_helpers.h`](firmware/lift_station_monitor/lift_station_monitor_helpers.h) | Compile-time constants, `AppState` struct definition, `extern` globals, and helper-function prototypes |
-| [`lift_station_monitor_helpers.cpp`](firmware/lift_station_monitor/lift_station_monitor_helpers.cpp) | Helper implementations: parsing, clamping, `notecardConfigure()`, `defineTemplates()`, `fetchEnvOverrides()`, `applyHubSetIfChanged()`, sensor reads |
-
-The `.ino` file is self-contained for the Arduino IDE (which compiles `.ino` + `.cpp` files in the same sketch folder together automatically); the split keeps the main sketch readable and puts reusable utilities in their own compilation unit.
-
-**Dependencies:**
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (`note-arduino` library). Install via Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`.
-
-### Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard configuration (`hub.set`, `card.transport` `wifi-cell-ntn` for cellular→satellite fallback, accelerometer disable) | `notecardConfigure` |
-| Notefile template registration | `defineTemplates` |
-| Env-var threshold fetch (every wake) | `fetchEnvOverrides` |
-| Re-issue `hub.set` when `summary_interval_min` changes | `applyHubSetIfChanged` |
-| Level sensor ADC read and % conversion | `readLevelPct` |
-| CT-based pump current measurement | `readPumpAmps` |
-| Float switch debounced read | `readFloatSwitch` |
-| Three-rule fault detection with cooldowns | `runDetectionCycle` |
-| Immediate-sync alert emission | `sendAlert` |
-| Hourly aggregated summary emission | `sendSummary` |
-| Persistent state across sleep cycles | `AppState` struct + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### Sensor reading strategy
-
-- **Level (4–20 mA transducer).** The firmware averages 64 ADC samples (with 500 microseconds inter-sample delay to allow the STM32 ADC input to settle) and maps the result to a 0–100% scale using calibration constants derived from the 150 Ω shunt physics: 4 mA → 745 counts, 20 mA → 3723 counts on a 12-bit, 3.3 V ADC. `analogReadResolution(12)` is called in `setup()` to enable 12-bit mode on the STM32L433.
-
-- **Pump current (SCT-013-030 CT).** Each CT produces an AC signal centered at VREF/2 by the bias resistor divider. The firmware first measures the DC bias over 256 bare `analogRead()` calls, then accumulates the squared deviation from that bias over 1024 `analogRead()` calls and takes the RMS. The actual sampling window is MCU/ADC-rate-dependent — the STM32L433's successive-approximation ADC completes each conversion in a few microseconds, so 1024 samples typically spans well under a millisecond at maximum rate; the integration window is not synchronized to the AC mains cycle. This is sufficient for detecting whether a pump is running or not, but does not constitute a calibrated true-RMS measurement. The SCT-013-030's specification is 1 V RMS per 30 A RMS, so `I_rms = V_rms × 30`. Both CT channels are read every cycle; a pump is considered running when its current reading exceeds `pump_on_amps`.
-
-- **Float switch.** A 5-reading majority-vote debounce (50 milliseconds total) filters contact bounce. The result is a single boolean: `true` if the float switch is indicating a high-water condition, `false` if normal.
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates give both files a fixed-width wire encoding, shrinking each Note by roughly 3–5× versus free-form JSON — meaningful over a multi-year deployment with 24 summary Notes per day per station.
-
-**`lift_alert.qo`** — Emitted immediately (within seconds for cellular, minutes for satellite) when any fault rule trips. Example:
-
-```json
-{
- "file": "lift_alert.qo",
- "body": {
- "alert": "pump_fail_to_start",
- "level_pct": 87.4,
- "pump1_amps": 0.2,
- "pump2_amps": 0.1,
- "float_sw": false
- },
- "sync": true
-}
-```
-
-Fields:
-- `alert`: one of `"pump_fail_to_start"`, `"pump_clog"`, or `"high_water_alarm"`.
-- `level_pct`: wet-well fill at time of alert.
-- `pump1_amps`, `pump2_amps`: instantaneous current draw on each motor supply at time of alert.
-- `float_sw`: whether the high-water float switch is closed (active) at time of alert.
-
-**`lift_summary.qo`** — Queued every 60 minutes (or `summary_interval_min`) and flushed in the next Notecard outbound session. Example:
-
-```json
-{
- "file": "lift_summary.qo",
- "body": {
- "level_pct": 42.1,
- "level_avg_pct": 38.6,
- "pump1_amps_avg": 14.2,
- "pump2_amps_avg": 0.0,
- "pump1_run_min": 18.0,
- "pump2_run_min": 0.0,
- "float_sw": false,
- "alert_count": 0,
- "level_faults": 0,
- "ct1_faults": 0,
- "ct2_faults": 0
- }
-}
-```
-
-Fields:
-- `level_pct`: instantaneous reading at the end of the summary window.
-- `level_avg_pct`: mean of all 60 samples collected during the window (or fewer if `summary_interval_min` was changed mid-window).
-- `pump1_amps_avg`, `pump2_amps_avg`: average current draw per pump across the window.
-- `pump1_run_min`, `pump2_run_min`: how many minutes each pump was detected running (useful for lead/lag duty balance assessment).
-- `float_sw`: whether the float switch is closed at summary time.
-- `alert_count`: how many alerts fired during this window (0 = clean window, >0 = trouble).
-- `level_faults`, `ct1_faults`, `ct2_faults`: count of samples where the sensor ADC returned an out-of-range value (open circuit, short, rail saturation, or other hardware fault). If nonzero, the associated average field is degraded by hardware issues, not actual station state.
-
-### Low-power strategy
-
-Each wake cycle lasts only a few seconds: read sensors (~300 milliseconds for CT RMS), evaluate rules, queue or sync Notes, then call `NotePayloadSaveAndSleep`. The Notecard cuts power to the Cygnet host entirely via the ATTN pin connection on the Notecarrier CX; the host draws essentially zero from the rail during sleep. Notecard for Skylo itself sits in its own [low-power idle](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) state between radio sessions (~8 µA @ 5 V), regardless of which radio it last used.
-
-**Sync strategy.** The Notecard runs in [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `mode:"periodic"` — the correct choice for a duty-cycled sensor node. In `periodic` mode the radio is fully off between sessions; the Notecard wakes on the configured `outbound` timer, ships queued Notes, then returns to low-power idle. `notecardConfigure` sets `outbound:60` (60-minute outbound sync interval) and `inbound:120` (2-hour env-var pull cadence). These cadences are deliberately decoupled from the 60-second sample interval: sensor reads accumulate in the in-flight summary window, and the Notecard opens a radio session only once per hour for summaries. Alert Notes set `sync:true`, which causes the Notecard to open a session as soon as the host queues the Note — bypassing the `outbound` timer entirely. `notecardConfigure` also issues a one-time [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) `{"method":"wifi-cell-ntn"}` so the Notecard prefers WiFi, then cellular, then Skylo satellite (NTN) — the fallback that lets the same firmware serve both in-coverage and beyond-coverage stations. For stations expected to operate over satellite, `outbound:60` is a reasonable starting point, but the `inbound:120` cadence should be reviewed: each inbound satellite check consumes approximately 50 bytes of bundled satellite data, and twice-hourly polling is a significant fraction of the 10 KB bundled allocation. Raise `inbound_interval_min` to `240` or higher via the Notehub env-var panel for satellite stations. If the outbound summary rate is also reduced (via `summary_interval_min`), set both env vars together. The firmware re-issues `hub.set` with the updated outbound and inbound values whenever either variable changes — no firmware reflash needed.
-
-Sampling cadence (60 seconds) and transmission cadence (default 60 minutes, set by `summary_interval_min`) are deliberately decoupled: each summary window's sensor reads feed one summary Note, but only alert Notes bypass the outbound timer. The firmware re-issues `hub.set` if an operator changes `summary_interval_min` via Notehub, so the Notecard's outbound window stays aligned with the summary rate automatically. At nominal operating conditions a single-station deployment generates one outbound session per summary interval plus occasional alert sessions — a tiny fraction of Notecard for Skylo's included 500 MB of cellular data, and (for stations operating over satellite) a meaningful but manageable fraction of its bundled 10 KB satellite allocation.
-
-### Retry and error handling
-
-- **Cold-boot I²C race.** The first `hub.set` uses `notecard.sendRequestWithRetry(req, 10)` to paper over the window where the host MCU comes up before the Notecard's I²C listener is ready — this is a documented condition in the `note-arduino` library.
-- **Env-var fetch failure.** `fetchEnvOverrides` uses `requestAndResponse` and silently returns on a NULL response. A failed `env.get` on any given wake retains the last valid threshold values from persistent state. No alert is emitted; the system continues sampling at the previously known thresholds.
-- **Alert deduplication.** Per-alert cycle-based cooldown counters (30 cycles × 60 seconds = 30 minutes) prevent a sustained fault condition from paging the on-call engineer repeatedly. Each alert type re-arms independently; a pump fail-to-start and a float-switch alarm can page simultaneously.
-- **State recovery.** If `NotePayloadRetrieveAfterSleep` fails (first power-up, Notecard flash corruption), the firmware zero-initializes all state and re-runs `hub.set` and `note.template`, both of which are idempotent at the Notecard. Summary-window accumulators reset to zero; at most one summary window of data is lost.
-- **`note.add` retry and accumulator preservation.** `sendAlert` uses `notecard.sendRequestWithRetry(req, 5)` so a transient I²C hiccup gets a second chance before the call is declared failed; alert cooldowns are armed only on a confirmed success, meaning a failed send leaves the cooldown at zero and the alert is retried on the next 60-second cycle. `sendSummary` checks the return value of `notecard.sendRequest()` and the caller clears summary accumulators **only on success** — if the Notecard is temporarily unreachable the accumulated window data is preserved and the send is retried on the next cycle. `note.template` registration is retried on every wake until both templates succeed (see `g_state.templates_registered`). The remaining narrower gap: neither `sendAlert` nor `sendSummary` inspects the Notecard response's `err` field, so a Notecard-side error that does not produce a NULL response is not surfaced to the host log. Production deployments that need end-to-end confirmation should add `notecard.responseError(rsp)` checks around the `sendRequest` return path.
-- **Sensor open/short detection — partial coverage.** The level sensor path is fault-checked at the ADC: an averaged count below `LEVEL_ADC_FAULT_LO` (≈ 645) flags an open loop (sensor unplugged, broken vented cable, lost loop power), and a count above `LEVEL_ADC_FAULT_HI` (≈ 3823) flags a shorted loop or severe overpressure. Both conditions emit `LEVEL_INVALID_SENTINEL` (-9999) instead of a clamped 0 % / 100 % reading and increment `level_faults` in the hourly summary. The CT channels are similarly guarded: a bias point outside `[CT_BIAS_MIN, CT_BIAS_MAX]` (1024–3072 counts) flags a broken bias-divider resistor or a CT terminal shorted to a rail, and any sample inside `CT_RAIL_MARGIN` of 0 or 4095 during the RMS window flags rail saturation (shorted secondary, severely over-ranged input). Faulted CT samples emit `CT_INVALID_SENTINEL` and increment `ct1_faults` / `ct2_faults`. **The remaining gap** is an open CT secondary winding combined with an intact bias divider: that condition reads ≈ Vref/2 with near-zero variance, indistinguishable in software from a legitimately idle pump. Operators should treat a sustained `ct*_faults` count or an unexplained high-water alarm with reported zero pump current as a prompt for a physical inspection.
-
-### Key code snippet 1: CT RMS current measurement
-
-Two-phase CT read: establish the bias point, then compute RMS of the AC deviation.
-
-```cpp
-// Step 1: measure DC bias (mid-rail ≈ 1.65 V in 12-bit counts)
-long bias_sum = 0;
-for (int i = 0; i < CT_BIAS_SAMPLES; i++) {
- bias_sum += analogRead(pin);
-}
-float bias = (float)bias_sum / (float)CT_BIAS_SAMPLES;
-
-// Step 2: RMS integration (1024 samples; duration is MCU/ADC-rate-dependent)
-double sq_sum = 0.0;
-for (int i = 0; i < CT_RMS_SAMPLES; i++) {
- float s = (float)analogRead(pin) - bias;
- sq_sum += (double)s * s;
-}
-float v_rms = (float)sqrt(sq_sum / CT_RMS_SAMPLES) * (3.3f / 4095.0f);
-float amps = v_rms * CT_AMPS_PER_VOLT; // 30 A per 1 V RMS
-```
-
-### Key code snippet 2: three-rule fault detection with cooldown
-
-```cpp
-// Rule 2: Pump fail-to-start — level deep, no pump running
-if (!any_on && level_pct >= g_high_level_pct &&
- g_state.cooldown_fail_to_start == 0) {
- sendAlert("pump_fail_to_start", level_pct, p1_a, p2_a, float_sw);
- g_state.cooldown_fail_to_start = ALERT_COOLDOWN_CYCLES;
- g_state.alert_count++;
-}
-
-// Rule 3: Pump clog — pump running, level still rising
-float delta = level_pct - g_state.prev_level_pct;
-if (any_on && delta >= g_rising_rate_pct && g_state.cooldown_clog == 0) {
- sendAlert("pump_clog", level_pct, p1_a, p2_a, float_sw);
- g_state.cooldown_clog = ALERT_COOLDOWN_CYCLES;
- g_state.alert_count++;
-}
-```
-
-### Key code snippet 3: immediate-sync alert
-
-`sync:true` tells the Notecard to bypass the hourly outbound window and wake the radio immediately — critical for a fault event where minutes matter.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", NOTEFILE_ALERT);
-JAddBoolToObject(req, "sync", true); // bypass outbound interval; wake radio now
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", type);
-JAddNumberToObject(body, "level_pct", level_pct);
-JAddNumberToObject(body, "pump1_amps", p1_a);
-JAddNumberToObject(body, "pump2_amps", p2_a);
-JAddBoolToObject(body, "float_sw", float_sw);
-// Retry briefly so a transient I²C hiccup doesn't silently drop a fault note.
-// Returns true if the Notecard accepted the request; cooldown is armed only on
-// success so a failed send is retried next cycle rather than suppressed for 30 min.
-bool ok = notecard.sendRequestWithRetry(req, 5);
-```
-
-## 8. Data Flow
-
-
-
-**Collected (every 60 seconds):** wet-well fill level (%), pump 1 RMS current (A), pump 2 RMS current (A), float switch state.
-
-**Summarized (every 60 minutes):** instantaneous and average level, average current per pump, per-pump runtime minutes in the window, float switch state, alert count in the window.
-
-**Transmitted:**
-- `lift_alert.qo` — emitted immediately on any rule trip with `sync:true`. Each alert carries the triggering level, both pump currents, and float switch state so the operator can assess severity without waiting for the next summary.
-- `lift_summary.qo` — queued hourly and flushed in the Notecard's next outbound cellular session (default 60 minutes after the previous one). One Note per hour per station, 24 Notes per day.
-
-**Alerts trigger on:**
-- `high_water_alarm` — float switch contact closes (highest priority; hardware-level confirmation that the wet well is dangerously full regardless of level sensor state).
-- `pump_fail_to_start` — wet-well level is at or above `high_level_pct` and no pump is drawing current above `pump_on_amps`. Indicates a pump that is not responding to its float control signal or has a failed contactor.
-- `pump_clog` — at least one pump is drawing current (≥ `pump_on_amps`) but the wet-well level is rising at ≥ `rising_rate_pct` per cycle. Indicates that the pump is not moving water effectively — consistent with a discharge obstruction, a failed or partially closed check valve, a worn impeller, or high inflow exceeding pump capacity.
-
-**Routed:** `lift_alert.qo` goes to a real-time notification channel. `lift_summary.qo` goes to a long-term store. Notehub applies project routes without any filter logic needed in the route itself, because the separation of Notefiles at the source is already the filter.
-
-## 9. Validation and Testing
-
-**Expected steady-state.** In normal operation a properly functioning lift station generates one `lift_summary.qo` Note per summary interval (default 60 minutes) and zero `lift_alert.qo` Notes. A healthy summary shows `level_avg_pct` well below `high_level_pct`, `alert_count: 0`, and at least one pump with non-zero runtime in the window. Lead/lag stations commonly show one pump carrying the entire load in a quiet hour — zero runtime on the lag pump during a single interval is normal, not an alarm condition.
-
-**Checking events in Notehub.** After flashing and powering the board:
-1. Open **Notehub > Devices**.
-2. Click your Notecard; the **Events** tab shows all `lift_alert.qo` and `lift_summary.qo` Notes received.
-3. Click any event to expand its JSON payload and inspect the body fields (level, current, alert type, fault counts).
-
-**Bench fault simulation.** The firmware clamps `high_level_pct` to a minimum of **1.0 %** and `rising_rate_pct` to a minimum of **0.1 %**, so setting either to `0.0` in Notehub has no effect — the firmware retains its last valid value. Use the compile-time test flags below for guaranteed bench triggering, or follow the hardware-assisted procedures:
-
-- `pump_fail_to_start` — **compile-time flag (recommended).** Rebuild the firmware with `-DBENCH_FORCE_LEVEL_PCT=50.0` in your build flags (Arduino IDE: add `#define BENCH_FORCE_LEVEL_PCT 50.0` at the top of the sketch, or pass via `arduino-cli` with `--build-property "compiler.cpp.extra_flags=-DBENCH_FORCE_LEVEL_PCT=50.0"`). The macro injects a synthetic 50 % level reading on every cycle, bypassing the ADC. With `high_level_pct` set to `1.0` in Notehub env vars and no pump CT drawing current above `pump_on_amps`, the rule fires on the next sample. A `[BENCH]` line in the serial log confirms the override is active. **Remove the flag before flashing to a deployed station.**
-- `pump_fail_to_start` — **hardware-assisted bench alternative.** Set `high_level_pct` to `1.0`. Inject a mid-range voltage on A0 using a 10 kΩ / 10 kΩ resistor divider from +3V3 to GND (tap the midpoint, 1.65 V, through the 150 Ω shunt to A0, gives ≈ 2048 ADC counts ≈ 50 % fill level). With A1 and A2 reading near-zero (CTs clamped on a dead conductor), the rule fires on the next cycle after Notehub pushes the env-var update.
-- `pump_clog` — **compile-time flag (recommended).** Rebuild with `-DBENCH_CLOG_DELTA=2.0`. The macro substitutes a synthetic +2.0 %/cycle delta for the measured level difference inside `runDetectionCycle()`. With `rising_rate_pct` at its default 2.0 % and a CT reading above `pump_on_amps` for two consecutive cycles (clamp both CT jaws on a live conductor), the rule fires. **Remove the flag before flashing to a deployed station.**
-- `high_water_alarm` — Briefly short pin D2 to GND (jumper wire or a bench push-button to GND) to simulate the float switch contact closing. No env-var change or firmware rebuild required.
-
-**Power validation with Mojo.** Insert the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline on the 5 V rail feeding the Notecarrier CX during a bench run; Mojo measures the entire Notecarrier subsystem (Notecard + Cygnet + carrier regulators), not the Notecard alone. Approximate per-state draw at the 5 V rail ([low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)):
-
-Notecard for Skylo's draw depends on which radio is active for a given session, but the idle and host-active states are identical regardless of network. Cellular and satellite sessions are similar in magnitude (~250 mA average from the onboard BG95-S5 modem), with brief higher peaks — a 2G transmit burst can momentarily pull nearly 2 A for a few milliseconds.
-
-| Operating state | Notecard for Skylo | Cygnet host | 5 V rail total |
-|---|---|---|---|
-| Deep sleep — host cut by ATTN, Notecard idle | ~8 µA | ~0 µA | ~10 µA |
-| Host active — sensor reads + I²C (~300 milliseconds per 60 seconds cycle) | ~8 µA | ~20–30 mA | ~20–30 mA |
-| Network session only, host asleep (batched summary) — cellular or satellite | ~250 mA avg, brief multi-hundred-mA (up to ~2 A for a few ms) peaks | ~0 µA | ~250 mA avg |
-| Host active + immediate network session (alert `sync:true`) | ~250 mA avg | ~20–30 mA | ~270 mA avg |
-
-**24-hour energy budget (cellular operation, powered continuously from 120 VAC supply, illustrative numbers; network session length dominates and varies with signal conditions — satellite sessions land in the same magnitude):**
-
-- Idle: ~10 µA × ~24 h ≈ **~0.25 mAh/day** (the Notecard idle plus carrier-board quiescent draw, accumulated for the ~99.5% of the day the host is gated off).
-- Host wakes: ~25 mA × 0.3 seconds × 60 wakes/hr × 24 hours ≈ 10,800 mA·s ≈ **~3 mAh/day** (10,800 mA·s ÷ 3600 s/hr).
-- Hourly outbound session: ~250 mA avg × ~10 seconds × 24 sessions/day ≈ 60,000 mA·s ≈ **~17 mAh/day** (assumes a typical LTE-M session including network registration; a clean network and queued-template summary commonly lands in the 5–15 seconds range, while marginal coverage — or a fallback to satellite — stretches it well past 30 seconds).
-- Occasional alerts (assume 2/day): ~250 mA × ~10 seconds × 2 ≈ **~1.4 mAh/day**.
-
-**Total: roughly ~22 mAh/day in steady state, with hourly cellular sessions dominating the budget.** Weak-signal sites where the modem camps in registration can easily push this 2–3× higher. The MeanWell HDR-15-5 (85–264 VAC input, 5 VDC 3 A output) provides continuous power from the station's 120 VAC control supply, so energy budgeting is not the deployment constraint, but validating these current draws with Mojo confirms the sleep/wake architecture is working and surfaces signal-quality issues early.
-
-**Expected Mojo trace over 24 hours:**
-- Dominant pattern: 60-second intervals of near-zero current (~10 µA, host and Notecard idle).
-- Every 60 seconds: 300 milliseconds spike at ~25 mA (host wakes, reads sensors, issues I²C).
-- Every ~60 minutes: longer spike at ~250–350 mA lasting ~2–5 seconds (Notecard modem session for summary).
-- Occasional taller/longer spikes: alerts firing with `sync:true` (variable timing, depends on fault events).
-
-**Troubleshooting constant mid-level draw:** If Mojo shows a continuous ~10–50 mA rather than this spike pattern, the ATTN pin is likely not cutting host power. The Notecarrier CX pairs the Notecard's `ATTN` interrupt with an `EN` input that gates the on-board host's 3.3 V rail; ATTN-driven host power gating requires those two pins be tied together. Verify that connection (check your Notecarrier wiring diagram) before assuming a firmware bug — the HST/NC DIP switch on the Notecarrier CX selects only which device is connected to the USB serial interface and has no effect on host power. Mojo is the fastest way to confirm sleep architecture is working before the unit goes underground.
-
-## 10. Troubleshooting
-
-**Firmware won't compile.** Ensure you have the correct board package and library versions:
-```bash
-arduino-cli board install stm32duino:STM32:1.11.0
-arduino-cli lib install "Blues Wireless Notecard"
-```
-
-**Notecard not appearing in Notehub.** Check the firmware serial log:
-1. Open a serial terminal at 115200 baud.
-2. Power the Notecarrier CX; watch for `Notecard configured` and `hub.set` messages.
-3. Verify `PRODUCT_UID` in `lift_station_monitor_helpers.h` is not empty and matches your Notehub project UID.
-4. If `hub.set` fails, the Notecard may not have cellular or WiFi coverage. Check antenna connection and signal strength (use `card.signal` request via serial via the [Notecard CLI](https://dev.blues.io/tools-and-sdks/notecard-cli/)).
-
-**No events appearing in Notehub after 5 minutes.** Check:
-1. **Network availability** — does the Notecard have cellular or WiFi access? Use the serial log or `card.signal` to verify.
-2. **Event payload size** — when Notecard for Skylo is transmitting over satellite (NTN), Notes exceeding 256 bytes are silently dropped. Check the serial log for `note.add` success/failure status.
-3. **Outbound sync window** — summaries are queued and synced every 60 minutes by default. Alerts fire immediately with `sync:true`, so if you've triggered an alert and the Notecard has coverage, it should appear within 60 seconds. If not, check the serial log for `sendAlert` and `note.add` output.
-
-**Constant non-zero current when powered (Mojo shows ~10–50 mA instead of spike pattern).** The ATTN pin is not cutting host power:
-1. Verify the Notecarrier CX `ATTN` and `EN` pins are tied together (check the specific carrier revision wiring diagram).
-2. Re-flash the firmware — if `setup()` is crashing before reaching `NotePayloadRetrieveAfterSleep`, the host MCU may stay powered.
-3. If still stuck, try a cold power-off (disconnect 120 VAC for 10 seconds), then reconnect.
-
-**Sensors read all-zero or invalid values.** Check:
-1. **Level sensor (A0):** Verify the 150 Ω shunt resistor is connected in series between the sensor's negative wire and GND. The ADC should read 745–3723 counts (0–100 %). If it reads <745, the loop is open or the shunt is missing/damaged.
-2. **CT channels (A1, A2):** Verify the 10 kΩ bias divider and 10 µF decoupling cap are correctly installed. The DC bias should read around 2048 counts (VREF/2 ≈ 1.65 V). If the bias is outside 1024–3072, the resistors are mismatched or the CT is shorted.
-3. **Float switch (D2):** The pin is `INPUT_PULLUP`, so a logic LOW (GND) is active (alarm). Jumper D2 to GND to test.
-
-## 11. Limitations and Next Steps
-
-This reference design covers the bulk of typical municipal lift stations, but it deliberately stops short of full SCADA replacement. The simplifications below are scope choices — places where a production deployment will want to add another sensor, another field-tunable, or another integration once a real utility starts running it.
-
-### Simplified for the POC
-
-The simplifications below are scope choices — each is a place where a production deployment will want to add another sensor, another field-tunable, or another integration once a real utility starts running it.
-
-**Level sensor accuracy assumes a clean, straight wet well.** The firmware maps ADC counts linearly to % fill using the transducer's pressure-to-level formula, assuming a uniform cross-section. Wet wells with irregular geometry or foaming conditions will read inaccurately; a field-calibrated offset via an environment variable is the production fix.
-
-**CT range and motor type.** The SCT-013-030 (30 A) suits single-phase motors up to ~5 HP at 230 V. Motors larger than 5 HP single-phase, or any three-phase motor, require a higher-ratio CT (for example, the SCT-013-060, 60 A / 1 V). Three-phase installations also require one CT per phase; the current sketch reads one CT per pump, which serves as a running/not-running indicator on a single leg but **does not produce true 3-phase RMS power.**
-
-**Fail-to-start detection is level-threshold only.** The firmware does not know what level the pump float controls are actually set to. The `high_level_pct` threshold is a firmware-side approximation of the hardware float-control setpoint; the two may not match unless calibrated after installation.
-
-**No discharge pressure or flow measurement.** The `pump_clog` rule fires on level-rising-while-running, which is a necessary but not sufficient condition for a clog — it also fires on genuine high-inflow conditions (heavy rain) or when both pumps are running and inflow exceeds combined capacity. Production deployments benefit from a discharge pressure sensor that can distinguish "pump is pumping but the line is blocked" from "pump is pumping but inflow is just overwhelming."
-
-**No SCADA integration.** The sketch is standalone. Most municipal lift stations already have a local RTU or telemetry unit, and integrating with that system — reading dry contacts from the existing SCADA outputs, or making the Notecard's data available to the local RTU — is outside the scope of this POC.
-
-**Satellite (NTN) operation caveats.** The firmware is identical everywhere, but when Notecard for Skylo falls back to the satellite link it carries material operational differences. Alert and summary Notes queue in the Notecard's local store and sync on the satellite session schedule — `sync:true` Notes are prioritized for the next available session, but locating a Skylo satellite and completing transmission takes several minutes, not seconds. Each Note must stay within the **NTN 256-byte maximum**; Notes exceeding this are silently dropped by the satellite network without transmission. Inbound syncs (env-var pulls) consume approximately 50 bytes of the 10 KB bundled satellite data allocation each, so the default 2-hour inbound cadence costs roughly 600 bytes/day. The Skylo-certified main antenna must be mounted outdoors with an unobstructed sky view (in the northern hemisphere, an unobstructed view of the southern sky) for the satellite link to work, so stations with the enclosure entirely below grade will need an above-grade cable run. Satellite operation is opt-in at the Notecard level: the firmware enables it by setting `card.transport` to `wifi-cell-ntn` — without an `ntn` transport mode the board would stay on cellular/WiFi only and never reach the satellite network. See [Section 2](#satellite-ntn-operation-considerations) and [Section 8](#9-validation-and-testing) for the full breakdown.
-
-**Mojo is bench-validation only.** The firmware does not read the Mojo's coulomb counter register over Qwiic. Adding a `mojo_mah` field to the hourly summary is a simple extension using the LTC2959 register map if fleet-level energy telemetry is valuable.
-
-### Production Next Steps
-
-Once a real utility is running the basic monitor, the following extensions are the natural progression — roughly from the most immediately useful to the most integration-dependent.
-
-**Per-station level calibration** is the first refinement: a `level_offset_pct` environment variable for wet-well geometry corrections, applied after the ADC-to-percent conversion.
-
-**Three-phase current support** adds `A3` for the third CT leg on 3-phase pumps and sums the squared contributions for a true 3-phase RMS reading.
-
-**A discharge pressure sensor on I²C** (for example, a 4–20 mA → I²C transducer) would distinguish a clog from high-inflow, reducing false positives from storm events.
-
-**[Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** enables over-the-air host firmware updates (on cellular/WiFi only), so threshold-algorithm improvements roll out to the fleet without a truck roll to each underground vault.
-
-**Pump cycle-count tracking** logs each pump start and stop (the transition from below to above `pump_on_amps`) to accumulate lifetime cycle counts and flag motors approaching their rated duty-cycle limits.
-
-**Integration with the municipal SCADA or CMMS** closes the loop: a `lift_alert.qo` Notehub route that creates a CMMS work order automatically, so the on-call response begins the moment the Notecard transmits, not the moment an engineer reads an SMS.
-
-## 12. Summary
-
-The on-call engineer who used to first hear about a sanitary sewer overflow from a citizen complaint on Monday morning now gets a page at 2 AM Saturday — wet well rising while a pump is running, or a float switch closing, or a pump that should be on but isn't. Three sensors, one Notecard for Skylo that carries cellular, WiFi, and satellite on a single module, and three rules running on a sleeping host turn an opaque concrete vault into a station that calls for help before it overflows — and the exact same hardware and firmware drops onto the station three blocks from city hall or the one at the edge of the service territory, reaching cellular where there's a tower and the Skylo satellite network where there isn't, with nothing to swap.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/municipal-wastewater-lift-station-monitor/)**
diff --git a/53-municipal-wastewater-lift-station-monitor/banner.png b/53-municipal-wastewater-lift-station-monitor/banner.png
deleted file mode 100644
index ba254c54..00000000
Binary files a/53-municipal-wastewater-lift-station-monitor/banner.png and /dev/null differ
diff --git a/53-municipal-wastewater-lift-station-monitor/diagrams/01-system-architecture.svg b/53-municipal-wastewater-lift-station-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index bac7c2c0..00000000
--- a/53-municipal-wastewater-lift-station-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/53-municipal-wastewater-lift-station-monitor/diagrams/02-wiring-assembly.svg b/53-municipal-wastewater-lift-station-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index ed04e78f..00000000
--- a/53-municipal-wastewater-lift-station-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,85 +0,0 @@
-
diff --git a/53-municipal-wastewater-lift-station-monitor/diagrams/03-data-flow.svg b/53-municipal-wastewater-lift-station-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index 2f277fb8..00000000
--- a/53-municipal-wastewater-lift-station-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/README.md b/54-multi-site-walk-in-cooler-energy-setpoint-monitor/README.md
index c2178146..92aafe16 100644
--- a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/README.md
+++ b/54-multi-site-walk-in-cooler-energy-setpoint-monitor/README.md
@@ -1,430 +1,5 @@
# Multi-Site Walk-In Cooler Energy & Setpoint Monitor
-
+A cellular reference design giving corporate operations teams a live view of temperature, compressor runtime, and door behavior across an entire walk-in cooler fleet, without touching operator WiFi.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Multi-Site%20Walk-In%20Cooler%20Energy%20%26%20Setpoint%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/cooler_monitor/](firmware/cooler_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a cellular [energy savings](https://blues.com/energy-savings/) reference design that gives corporate operations teams a live view into the temperature, compressor runtime, and door behavior of every walk-in cooler in their fleet, without touching a single franchisee or operator's WiFi network.
-
-## 1. Project Overview
-
-**The problem.** For a chain QSR (quick-service restaurant), convenience store, or grocery operator, the walk-in cooler is one of the most expensive assets in a store, and one of the least visible. Energy cost per location is second only to labor, and a significant fraction of that energy budget runs through the compressor of one or more walk-in boxes. Operators who get visibility into compressor runtime, door discipline, and temperature trends across their fleet can cut energy spend materially: a door held open five minutes longer than necessary during a busy lunch rush costs real money in wasted refrigeration, and a compressor running six hours a day instead of four because box air temperature has quietly drifted 2 °F above the corporate temperature target costs even more at scale.
-
-The harder problem is that an operator running 800 locations has 800 different network environments — franchisees on consumer ISPs, independent operators with POS (point-of-sale) systems that their IT vendors won't let anyone touch, convenience stores with back-office networks that preclude any new guest devices. Getting corporate visibility through all of that friction is the barrier that keeps most energy-monitoring pilots from becoming fleet-wide programs. The pilot sites get instrumented; the rollout stalls at 50.
-
-This project is the device that gets past that barrier. One SKU, cellular-connected, no IT ticket required. Stick a temperature probe inside the box, clamp a split-core current transformer on the compressor hot leg, mount a magnetic reed switch on the door, and you get a per-unit compressor energy proxy — compressor apparent kWh per summary window (per-day totals derived downstream by summing `kwh_window` records), door-open events, and temperature-to-target deviation — delivered to the [Blues Notehub](https://blues.com/notehub/) cloud service and routed wherever corporate needs it.
-
-**Why Notecard.** The project description says it plainly: corporate energy management can't touch 800 independent-operator or franchisee WiFi networks — each would be its own ticket. A cellular Notecard is one SKU the field can plug in without asking anyone for a WiFi password. That's not a convenience; it's the difference between a program that deploys at fleet scale and one that stays perpetually in pilot. Deploying on cellular also keeps the energy-monitoring data stream entirely off the POS network, which matters both for network security and for the operational reality that POS downtime is the one thing nobody is willing to accept as a side effect of an energy program. The [Notecard Cell+WiFi](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) variant keeps WiFi as an opportunistic fallback for the occasional site that can offer it, without compromising the cellular-first deployment model.
-
-
-
-**Deployment scenario.** A small weatherproof enclosure mounted in or near the cooler's refrigeration cabinet or mechanical room, powered from the cooler's dedicated circuit (typically 120VAC for single-phase residential-style units; many commercial condensing units run on 208/240VAC. See Limitations). Three sensor leads run into the box: a waterproof DS18B20 temperature probe positioned in open box air at mid-box height or along the return-air path (reading representative box air temperature away from the evaporator and door), a split-core CT clamped on one hot leg of the compressor's dedicated circuit, and a two-wire reed switch mounted on the door frame with a magnet on the door itself. No OEM cooperation, no cooler modification, no network integration required.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Inside the cooler's mechanical-room enclosure, the Cygnet STM32 host on the Notecarrier CX comes up every `sample_interval_sec` seconds (default 60), reads all three sensors, updates the per-window accumulators (kWh proxy, compressor runtime, door open seconds and event count), and runs the two alert rules. Then it either queues a summary [Note](https://dev.blues.io/api-reference/glossary/#note) for the next window or goes straight back to sleep. Between wakes the host is fully powered down via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) — the Notecard holds the sleep state and brings the host back when the next interval lands.
-
-**Notecard responsibilities.** Whatever the host queues, the Notecard takes care of. Notes wait in the on-device queue until the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes) opens a cellular (or opportunistic WiFi) session and flushes them in one batch. Anything tagged `sync:true` skips the queue and the radio comes up immediately. The Notecard also pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from Notehub on every inbound sync — setpoint targets, alert timers, and cadences all tunable from a browser, no firmware updates or field visits required.
-
-**Notehub responsibilities.** The Notecard's embedded global SIM lands events in [Blues Notehub](https://notehub.io), which ingests them, stores every one, and runs the project-level routes to your cloud application of choice. Summaries and alerts go to separate JSON-based [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) on purpose: `cooler_summary.qo` for the hourly telemetry that flows to BI or a long-term store, and `cooler_alert.qo` for the immediate notifications that need to land on a phone or a Slack channel — no filter logic inside the route, just two streams pointed at the right destinations. [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) and [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) make it easy to set a corporate temperature target per banner or region while still overriding the one flagship that runs tighter.
-
-**Routing to the cloud.** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations. Route configuration is project-specific — this project ships no downstream endpoint. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for setup details.
-
-## 3. Technical Summary
-
-### First Event in 30 Minutes
-
-1. **Assemble the hardware** (Section 3–4): [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) + [Notecard Cell+WiFi](https://shop.blues.com/products/notecard-cellular?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link), DS18B20 temperature probe, SCT-013-030 current transformer, reed switch door sensor, and 5V/2A power supply in a NEMA 4X enclosure.
-
-2. **Create a [Notehub](https://notehub.io) project** and copy your ProductUID.
-
-3. **Flash the firmware:**
- ```bash
- git clone https://github.com/blues/app-accelerators.git
- cd app-accelerators/54-multi-site-walk-in-cooler-energy-setpoint-monitor/firmware/cooler_monitor
- # Edit cooler_monitor.ino: paste your ProductUID into the PRODUCT_UID constant
- arduino-cli core install "STMicroelectronics:stm32"
- arduino-cli lib install "Blues Wireless Notecard" "OneWire" "DallasTemperature"
- arduino-cli compile --fqbn "STMicroelectronics:stm32:Nucleo_L433RC_P" .
- arduino-cli upload --fqbn "STMicroelectronics:stm32:Nucleo_L433RC_P" -p /dev/ttyACM0 .
- ```
-
-4. **Power the device** and wait ~120 seconds. In Notehub, open your project → Events tab. You will see two Notefiles appear: `cooler_summary.qo` (hourly telemetry) and `cooler_alert.qo` (threshold events). A typical summary event looks like:
- ```json
- {
- "temp_f": 35.4,
- "setpoint_f": 35.0,
- "compressor_amps": 9.2,
- "compressor_run_min": 38.0,
- "door_opens": 12,
- "door_open_sec": 187,
- "kwh_window": 0.418,
- "window_sec": 3612
- }
- ```
-
-5. **Set threshold environment variables** in Notehub (Fleet → Environment → click row, edit JSON):
- - `temp_alert_f`: 40 (fires when box temp exceeds this; default 40 °F)
- - `door_open_alert_sec`: 300 (fires when door open continuously for this many seconds; default 5 minutes)
- - Other optional vars: `sample_interval_sec`, `summary_interval_min`, `temp_setpoint_f`, `compressor_on_amps`, `volts_nominal`
-
-6. **Trigger a test alert**: Open the cooler door and leave it open for >5 minutes, then wait for the next sample cycle. A `door_open_long` alert will appear in `cooler_alert.qo` with `sync:true` (immediate delivery, not queued).
-
-### What You'll Have When You're Done
-
-After completing this project, you will deploy a complete cellular-connected walk-in cooler monitoring system consisting of:
-
-1. **Three non-invasive sensors** mounted on the cooler (temperature probe in the box, current transformer clamp on the compressor hot leg, magnetic reed switch on the door) — no electrical changes required, no cooperation from the cooler's OEM.
-
-2. **A local enclosure** (Notecarrier CX with onboard Cygnet STM32 host, Notecard Cell+WiFi, power supply, and bias/pull-up circuits) that samples all three sensors every 60 seconds and automatically sleeps between samples.
-
-3. **Notehub as your cloud backend**, storing and routing two streams of data:
- - **Telemetry**: One summary event per hour per cooler, containing averages (temperature, compressor amps) and totals (runtime minutes, door-open events, apparent kWh, window duration).
- - **Alerts**: Immediate notifications whenever the door is held open > 5 minutes or box temperature exceeds 40 °F, delivered via Notehub routes to your ops/facilities team.
-
-4. **No WiFi required** — cellular connectivity eliminates the IT ticket bottleneck that has stopped energy programs at hundreds of multi-location operators. Works on franchisee sites, POS-restricted networks, and anywhere a cell signal exists.
-
-5. **Configuration without reflashing** — threshold temperatures, alert timers, and sample/summary cadences are all tunable via Notehub environment variables; changes take effect on the device's next inbound sync.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "cooler_summary.qo",
- "body": {
- "temp_f": 35.4,
- "setpoint_f": 35.0,
- "compressor_amps": 9.2,
- "compressor_run_min": 38.0,
- "door_opens": 12,
- "door_open_sec": 187,
- "kwh_window": 0.418,
- "window_sec": 3612
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32 host — no separate MCU needed for this three-sensor combination. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular-first connectivity that bypasses site WiFi entirely; WiFi fallback for the occasional co-operative site. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Inline coulomb counter for bench validation of sleep/wake current profiles before field deployment. |
-| [Adafruit Waterproof DS18B20 Temperature Sensor (product 381)](https://www.adafruit.com/product/381) | 1 | Calibrated digital 1-Wire temperature probe (±0.5 °C accuracy) in a stainless steel enclosure; purpose-built for wet, food-service environments where a bare thermistor would corrode. |
-| 4.7 kΩ resistor, ¼W | 1 | Required pull-up for the DS18B20 1-Wire data line; without it the bus hangs. |
-| SCT-013-030 split-core CT, 30A / 1V RMS (e.g., [SparkFun SEN-11005](https://www.sparkfun.com/products/11005)) (current-output CT; add external burden resistor for 1 V_rms scaling) | 1 | Non-invasive current measurement; clamps over the compressor hot leg without breaking the circuit. 30A matches single-phase condensing units found in most walk-in box installations. |
-| TRRS 3.5 mm breakout (e.g., [SparkFun BOB-11570](https://www.sparkfun.com/products/11570)) | 1 | The SCT-013's output lead terminates in a TRRS plug; this breakout converts it to solderable pads. |
-| 10 kΩ 1% resistor (CT bias pair) | 2 | Voltage-divider bias circuit that centers the CT's AC signal at Vcc/2 (1.65 V) so the ADC sees only positive voltages. |
-| 10 µF electrolytic capacitor | 1 | Decouples the bias node, preventing the CT signal from modulating the divider mid-point. |
-| [Adafruit Magnetic Contact Switch / Door Sensor (product 375)](https://www.adafruit.com/product/375) | 1 | Normally-open reed switch in a pre-wired ABS enclosure; screws to the door frame while the included magnet mounts on the door itself. Closes below 13 mm separation, opens above — reliable over millions of cycles with no moving parts that wear. |
-| 10 kΩ resistor, ¼W | 1 | External pull-up for the reed switch GPIO (supplements or replaces the internal STM32 pull-up for noise immunity). |
-| AC/DC supply, 5V/2A (e.g., [MeanWell IRM-10-5](https://www.meanwell.com/Upload/PDF/IRM-10/IRM-10-SPEC.PDF)) | 1 | Derives 5V/2A (10 W) DC from the cooler's dedicated circuit. The IRM-10-5 accepts 85–264VAC wide-range input, covering both 120VAC and 208/240VAC single-phase supplies. During a cellular session the MBGLW's Quectel EG916Q-GL modem draws ~250 mA average (LTE Cat-1 bis); in areas where the modem falls back to GSM coverage it can spike to ~2 A for a few milliseconds — a burst the supply's output bulk capacitance absorbs within its 2 A continuous rating. Confirm the available supply voltage at the installation site before ordering. See Limitations for 24VAC/24VDC control-transformer installs. |
-| External multiband LTE antenna, SMA male (e.g., [SparkFun CEL-29622](https://www.sparkfun.com/lte-hinged-external-antenna-600mhz-2700mhz-sma-male.html), 600 MHz–2.7 GHz hinged whip) | 1 | **Mandatory for any metal-enclosure or metal mechanical-room installation.** A Notecard antenna inside a steel NEMA 4X box cannot radiate reliably. The 600 MHz–2.7 GHz pass-band covers every band the MBGLW's Quectel EG916Q-GL operates on (LTE Cat-1 bis B1/B2/B3/B4/B5/B7/B8/B12/B13/B18–B20/B25–B28/B66 plus the 2G/3G fallback bands). Do **not** substitute a GPS/GNSS-only antenna (1.55–1.62 GHz) — it will not radiate on the cellular bands. Route the u.FL-to-SMA pigtail (next row) through a cable gland in the enclosure wall; attach this external SMA antenna on the exterior side of the gland. |
-| u.FL-to-SMA bulkhead pigtail cable, ~150 mm (e.g., [Adafruit product 851](https://www.adafruit.com/product/851)) | 1 | Connects the Notecard's onboard u.FL antenna port to the external SMA panel-mount antenna. Required whenever an external SMA antenna is used — without this cable the antenna has no electrical path to the Notecard radio. A 150–200 mm length routes cleanly inside a ~6×4×2″ NEMA box without excess coiling. |
-| NEMA 4X enclosure, ~6×4×2″ | 1 | Moisture- and hose-down-rated housing suitable for the cold, wet environment of a walk-in mechanical room. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees and no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-
-
-**Safety — qualified personnel only.** This assembly connects to mains-voltage AC power. The AC/DC supply installation (wiring to the cooler's dedicated circuit) must be performed by a licensed electrician in compliance with applicable electrical codes (NEC Articles 100 and 110 and local amendments). Apply **lockout/tagout** procedures to the circuit breaker before opening any conduit or junction box. Once installed, all three sensors (temperature probe, CT clamp, reed switch) are electrically non-invasive — the CT clamps over the wire without breaking the circuit and is isolated from the mains conductor, but the supply wiring itself is not. Do not energize the enclosure until all wiring is complete and the enclosure lid is secured.
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi seats into the carrier's M.2 slot. **An external antenna is mandatory in a metal walk-in box or steel mechanical-room cabinet** — a Notecard antenna inside a metal enclosure cannot radiate reliably. Route a short u.FL-to-SMA pigtail from the Notecard's u.FL port through a cable gland in the NEMA 4X enclosure wall and attach it to the panel-mount SMA whip antenna on the exterior. The Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad for bench validation.
-
-Pin-by-pin connections:
-
-- **3V3** → DS18B20 red wire (VDD); top of the upper 10 kΩ CT bias resistor (the divider's high leg); top of the 10 kΩ door-switch pull-up resistor.
-- **GND** → DS18B20 black wire (GND); CT TRRS breakout sleeve (the CT's signal return); bottom of the lower 10 kΩ CT bias resistor (the divider's low leg); 10 µF capacitor negative terminal; one terminal of the reed switch (either terminal, since it is just a switch).
-- **D5** → DS18B20 yellow wire (data); 4.7 kΩ pull-up from D5 to 3V3 wired here.
-- **A0** → CT signal node. Build the bias network as a series divider — one 10 kΩ from +3V3 to the bias node, one 10 kΩ from the bias node to GND, so the bias node sits at Vref/2 ≈ 1.65 V. Connect the 10 µF capacitor from the bias node to GND (decouples the bias rail to AC ground; **not** in series with the signal). The CT TRRS breakout's tip lands on A0 (= the bias node); the breakout's sleeve lands on GND. A0 then sees 1.65 V DC plus the CT's AC swing — within the 0–3.3 V ADC window.
-- **D6** → one terminal of the reed switch; 10 kΩ pull-up from D6 to 3V3 (reed switch other terminal goes to GND, making the pin LOW when door closed and HIGH when door open under `INPUT_PULLUP`).
-- **SDA / SCL** → Notecard I²C (Notecarrier CX routes these to the M.2 slot internally; no external wiring needed).
-- **+VBAT** → Mojo `LOAD` output (bench use only); Mojo `BAT` input ← 5V DC output of the MeanWell IRM-10-5 (or equivalent 5V/2A AC/DC supply), which is wired to the AC service conductors in the NEMA 4X enclosure.
-
-**CT installation Note.** The SCT-013-030 is a split-core clamp; open the clamp, pass it over *one* hot leg of the compressor's circuit (not both; clamping both legs cancels the magnetic fields and you'll read zero). Route the TRRS lead back to the NEMA 4X enclosure through a cable gland. The compressor circuit is line-voltage; installation must be performed by a qualified electrician following applicable codes and lockout/tagout procedures. The CT itself is entirely non-contact and electrically isolated once clamped.
-
-**DS18B20 placement.** For accurate air temperature monitoring, position the stainless probe tip in open box air at mid-box height or along the return-air path, away from both the evaporator fan discharge and the door. This location gives the most representative reading of box air temperature, which is a useful proxy for stored-product conditions but is not a direct product core-temperature measurement. Avoid the evaporator fan discharge — it is the coldest zone in the box and reads several degrees below true representative air temperature; placing the probe there biases temperature-to-target deviation calculations low and will delay `temp_high` alerts. Avoid the door zone, which sees warm infiltration air on every open cycle. Keep the probe away from direct coil contact for the same reason.
-
-**Reed switch installation.** Mount the ABS switch body on the door frame and the magnet on the door, aligned so the two components are within 10 mm when the door is fully closed. Test continuity before closing the enclosure: pin D6 should read LOW with door closed and HIGH with door open in the Arduino serial terminal.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/cooler_monitor/cooler_monitor.ino` as the `PRODUCT_UID` constant before flashing.
-2. **Claim the Notecard.** Power the unit; on first cellular connection the Notecard associates with your project automatically.
-3. **Create Fleets.** The natural grouping for a multi-site cooler program is one [Fleet](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) per banner or region — every store in a given franchise territory typically shares the same setpoint targets and alert thresholds. [Smart Fleet](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) rules let you break out exceptions automatically (e.g., flagship stores with tighter temperature tolerances) without manual device reassignment.
-4. **Set environment variables.** All variables below are optional; firmware defaults apply until overridden. Any variable set in Notehub takes effect on the device's next inbound sync — no reflash required. **To set a variable in Notehub:** navigate to your project → Devices (or Fleet) → scroll down to Settings/Environment Variables → add or update the variable and save. The firmware configures the Notecard's `inbound` sync interval to `2 × summary_interval_min` (so 120 minutes at the default `summary_interval_min = 60`), which is when the Notecard pulls fresh env-var values from Notehub into its on-device cache. The host then picks the new values up on its next 60-second wake. **Worst-case time from "save in Notehub" to "value applied on the device" is therefore roughly the inbound interval (up to 120 minutes by default) plus one sample cycle (~60 seconds).** To exercise the full path quickly during commissioning, lower `summary_interval_min` (which also halves the inbound interval) or trigger an immediate session via `hub.sync` from the [Blues In-Browser Terminal](https://dev.blues.io/terminal/).
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_sec` | `60` | Seconds between sensor reads and state updates. |
- | `summary_interval_min` | `60` | Minutes between `cooler_summary.qo` Notes. Also re-applies `hub.set outbound` to keep cellular cadence in sync. The `kwh_window` field reflects energy accumulated across the scheduled sample intervals within this window. |
- | `temp_setpoint_f` | `35.0` | Target box temperature (°F); transmitted in every summary Note as `setpoint_f` so the corporate dashboard can compute drift = `temp_f − setpoint_f` without a separate lookup. |
- | `temp_alert_f` | `40.0` | Box temperature (°F) above which a `temp_high` alert fires. |
- | `door_open_alert_sec` | `300` | Continuous seconds the door has been open before a `door_open_long` alert fires (default 5 minutes). |
- | `compressor_on_amps` | `2.0` | Amps threshold above which the compressor is considered running for runtime and kWh accumulation. |
- | `volts_nominal` | `120.0` | Nominal line voltage (V) used in the apparent-power kWh estimate. |
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `cooler_alert.qo` (real-time delivery to a store-ops or facilities channel) and a second for `cooler_summary.qo` (to a long-term analytics store or BI tool). Keeping the two Notefiles separate at the source means they can fan out to entirely different destinations without any filter logic inside the route.
-
-## 7. Firmware Design
-
-Main sketch plus helper files: [`firmware/cooler_monitor/cooler_monitor.ino`](firmware/cooler_monitor/cooler_monitor.ino) (entry point, sample cycle), [`firmware/cooler_monitor/cooler_monitor_helpers.cpp`](firmware/cooler_monitor/cooler_monitor_helpers.cpp) (Notecard config, sensor reads, Note emission), and [`firmware/cooler_monitor/cooler_monitor_helpers.h`](firmware/cooler_monitor/cooler_monitor_helpers.h) (shared constants, types, and declarations).
-
-**Dependencies:**
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (`note-arduino` library). Install via Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`.
-- [`OneWire`](https://github.com/PaulStoffregen/OneWire) library. Install via Arduino Library Manager.
-- [`DallasTemperature`](https://github.com/milesburton/Arduino-Temperature-Control-Library) library. Install via Arduino Library Manager.
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard `hub.set` configuration (cold start + every wake until confirmed) | `hubConfigure()` |
-| Template registration for both Notefiles | `defineTemplates()` |
-| Env-variable fetch and clamp on every wake | `fetchEnvOverrides()` |
-| Re-apply `hub.set` if `summary_interval_min` changed | `applyHubSetIfChanged()` |
-| DS18B20 temperature read (timeout-polled, NaN sentinel) | `readBoxTempF()` |
-| CT two-pass RMS current read | `readCompressorAmps()` |
-| Reed-switch door-state read | `readDoorOpen()` |
-| Accumulation, alert evaluation, summary trigger | `runSampleCycle()` |
-| Immediate-sync alert emission | `sendAlert()` |
-| Queued summary emission | `sendSummary()` |
-| State persistence / sleep until next sample | `NotePayloadSaveAndSleep()` in `loop()` |
-
-### Sensor reading strategy
-
-**DS18B20.** The probe is configured in non-blocking mode (`setWaitForConversion(false)`). After `requestTemperatures()`, the firmware polls `isConversionComplete()` in an 850 milliseconds timeout loop rather than calling `delay(750)`. The host remains awake throughout the conversion — this is a timeout-polled read within the same wake cycle, not a pipelined conversion across sleeps, but the polling approach avoids hanging indefinitely if the sensor is slow to respond. If the probe is disconnected or returns a value below –55 °C or at 85 °C or above (including the 85.0 °C power-up sentinel the DS18B20 can emit on a bus fault), `readBoxTempF()` returns `NAN`; the summary then emits `–9999` as a sentinel rather than a misleading zero, so downstream analytics can distinguish "sensor failed" from a genuine measurement.
-
-**Current transformer.** The SCT-013-030 outputs an AC signal centered at 0 V. A two-resistor 10 kΩ voltage divider from 3V3 to GND, with a 10 µF decoupling capacitor, creates a stable 1.65 V DC offset (Vcc/2) at the ADC pin. The firmware uses a two-pass algorithm: pass one samples the ADC continuously for 150 milliseconds to compute the DC mean; pass two samples for another 150 milliseconds and computes the RMS of the AC component around that mean. At 60 Hz one mains cycle is 16.7 milliseconds, so a 150 milliseconds window covers approximately nine complete cycles — a sufficient sample of the actual mains waveform for a well-conditioned RMS result. Readings below 0.15 A are floored to zero to suppress ADC noise when the compressor is off.
-
-**Reed switch.** A simple `digitalRead` with `INPUT_PULLUP`. The switch is normally open — it closes (pulling D6 LOW) only when the door is shut and the magnet is within 13 mm of the sensor. Door-open events are edge-detected: a transition from `prevDoorOpen = 0` to `doorOpen = 1` increments the event count.
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates let the Notecard store Notes as fixed-length binary records rather than free-form JSON, shrinking on-wire payload size by roughly 3–5×. At 24 summary Notes per box per day across a thousand-store fleet, the difference compounds. When you view events in Notehub (Events tab), they appear as human-readable JSON; the binary template format is internal storage only.
-
-**Template type encoding** (used by the firmware in `defineTemplates()`): `14.1` = IEEE 754 4-byte float; `14` = 4-byte signed integer; `12` = 2-byte signed integer; `22` = 2-byte unsigned integer. These are specified when registering a template to the Notecard so it knows how to pack and unpack each field.
-
-`cooler_summary.qo` (per summary window, queued):
-
-```json
-{
- "file": "cooler_summary.qo",
- "body": {
- "temp_f": 35.4,
- "setpoint_f": 35.0,
- "compressor_amps": 9.2,
- "compressor_run_min": 38.0,
- "door_opens": 12,
- "door_open_sec": 187,
- "kwh_window": 0.418,
- "window_sec": 3612
- }
-}
-```
-
-Field semantics: `temp_f` and `compressor_amps` are **window averages** (sum of valid readings ÷ valid-reading count). `compressor_run_min`, `door_open_sec`, `door_opens`, and `kwh_window` are **window totals**. `window_sec` is the sum of the scheduled sample intervals that elapsed during this window (sleep time only, awake time spent sampling is excluded). When `sample_interval_sec` does not evenly divide `summary_interval_min × 60`, the window overshoots by at most one sample period. Downstream tools should use `window_sec` as the denominator for any energy-rate or duty-cycle calculation (e.g. average watts = `kwh_window / window_sec × 3 600 000`), noting that actual wall-clock elapsed time is marginally longer than `window_sec` due to excluded awake time. `setpoint_f` carries the current Notehub-configured corporate target (not a value read from the cooler controller) so a dashboard can compute deviation = `temp_f − setpoint_f` per record without a separate lookup. `–9999` in `temp_f` signals a sensor fault (e.g. probe disconnected).
-
-`cooler_alert.qo` (immediate, `sync:true` bypasses outbound cadence):
-
-```json
-{
- "file": "cooler_alert.qo",
- "body": {
- "alert": "door_open_long",
- "temp_f": 38.1,
- "amps": 10.4,
- "door_open_sec": 312
- },
- "sync": true
-}
-```
-
-### Low-power strategy
-
-All sampling cadence (every 60 seconds by default) and transmission cadence (every 60 minutes) are decoupled. After each sample cycle, `NotePayloadSaveAndSleep` serializes the `AppState` struct into Notecard flash and then issues [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to cut power to the host MCU entirely for `sample_interval_sec` seconds. The Notecard then idles in its own [low-power mode](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) between cellular sessions. Because the walk-in cooler is powered from 120VAC, the absolute mAh budget is not the constraint, but the pattern still matters for enclosure thermal management and for portability to future battery-assisted variants.
-
-### Retry and error handling
-
-- **`hub.set` is retried on every wake until confirmed.** `hubConfigure()` now returns `bool`. The first call runs on cold start; its result is stored in `state.hubSetConfirmed`. If that call fails (e.g. the STM32 host comes up before the Notecard is ready on I²C and `sendRequestWithRetry` exhausts its 10 attempts), every subsequent warm wake includes an `else if (!state.hubSetConfirmed)` branch that retries `hubConfigure()` unconditionally — independent of whether `env.get` succeeds. Only after `hubSetConfirmed` is set does the device fall back to the lighter `applyHubSetIfChanged()` path, which re-issues `hub.set` solely when `summary_interval_min` changes. This guarantees the device cannot remain permanently unassociated while silently accumulating Notes in its local queue.
-- `env.get` checks the `err` field on the response before accessing `body` — a failed inbound sync (no `body`) leaves the compile-time defaults in place rather than corrupting config.
-- The two alert types carry independent cooldown timers (`doorAlertCooldownSec`, `tempAlertCooldownSec`) measured in wall-clock seconds so timing stays accurate across `sample_interval_sec` changes. Each alert can fire at most once per 30-minute window, preventing a temporarily-open door or a sluggish compressor from firing dozens of identical alerts on consecutive wakes.
-- If `summary_interval_min` changes via Notehub env var, `applyHubSetIfChanged()` re-issues `hub.set` with the updated outbound cadence so the Notecard's cellular session timing tracks the new summary period rather than drifting. This path runs only after `hubSetConfirmed` is true.
-
-### Key code snippet 1: template definition
-
-Template type hints tell the Notecard how to pack each field (see "Template type encoding" above). `door_open_sec` and `window_sec` use `14` (4-byte signed integer) rather than `12` (2-byte signed integer, max ~9 hours) because large summary intervals or a stuck door can accumulate values that overflow a 2-byte integer. Conversely, `door_opens` uses `12` (2-byte signed, up to ~32k events) since a realistic window rarely exceeds 50 door cycles. For complete field-type reference, see the [Blues template data-type guide](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#understanding-template-data-types).
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "cooler_summary.qo");
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "temp_f", 14.1); // 4-byte float
-JAddNumberToObject(body, "setpoint_f", 14.1); // 4-byte float
-JAddNumberToObject(body, "compressor_amps", 14.1); // 4-byte float
-JAddNumberToObject(body, "compressor_run_min", 14.1); // 4-byte float
-JAddNumberToObject(body, "door_opens", 12); // 2-byte signed int
-JAddNumberToObject(body, "door_open_sec", 14); // 4-byte signed int
-JAddNumberToObject(body, "kwh_window", 14.1); // 4-byte float
-JAddNumberToObject(body, "window_sec", 14); // 4-byte signed int
-notecard.sendRequest(req);
-```
-
-### Key code snippet 2: immediate-sync alert
-
-`sync:true` bypasses the outbound cadence — the Notecard wakes the radio immediately.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "cooler_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "door_open_long");
-JAddNumberToObject(body, "temp_f", 38.1);
-JAddNumberToObject(body, "amps", 0.0);
-JAddNumberToObject(body, "door_open_sec", 312);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 3: persist state and sleep
-
-`NotePayloadSaveAndSleep` serializes the struct to Notecard flash and then uses `card.attn` to cut VBAT to the host MCU. The next wake enters `setup()` fresh; `NotePayloadRetrieveAfterSleep` rehydrates the state.
-
-```cpp
-NotePayloadDesc outPayload = {0, 0, 0};
-NotePayloadAddSegment(&outPayload, SEG_STATE, &state, sizeof(state));
-NotePayloadSaveAndSleep(&outPayload, cfgSampleSec, NULL);
-```
-
-## 8. Data Flow
-
-
-
-**Collected** every `sample_interval_sec` (default 60 seconds): box air temperature (°F), compressor RMS amps, door open/closed state.
-
-**Accumulated** within each summary window: compressor apparent kWh (V × I × t / 1000, compressor hot leg only), compressor runtime minutes, total door-open seconds, and door-open event count (rising-edge transitions).
-
-**Transmitted:**
-- `cooler_summary.qo` — one record per `summary_interval_min` (default approximately once per hour), queued locally and flushed during the Notecard's scheduled outbound cellular session. `temp_f` and `compressor_amps` are **window averages** (mean over all valid reads during the window). `compressor_run_min`, `door_open_sec`, `door_opens`, and `kwh_window` are **window totals**. `window_sec` is the sum of scheduled sample intervals for the window (sleep time only, actual wall-clock elapsed time is marginally longer due to excluded awake time) — use it as the denominator for energy-rate calculations. `setpoint_f` carries the current Notehub-configured corporate target so downstream tools can compute deviation = `temp_f − setpoint_f` per record without a separate lookup. `–9999` signals a sensor fault for `temp_f` where `NAN` would be meaningless in JSON.
-- `cooler_alert.qo` — emitted only on a threshold trip, with `sync:true` to bypass the outbound timer. Alert cooldown prevents more than one alert per type per 30-minute window.
-
-**Routed.** Both Notefiles arrive at Notehub and from there to whichever downstream routes the project configures. The two filenames are deliberately different so they can be fanned out to different destinations at different urgencies without any route-level filtering.
-
-**Alert triggers:**
-- `door_open_long` — door has been continuously open for at least `door_open_alert_sec` seconds (default 5 minutes). Fires repeatedly at most once per 30-minute cooldown window while the door remains open — one page-out per half hour is enough for store ops to investigate.
-- `temp_high` — box temperature exceeds `temp_alert_f` (default 40 °F). The alert carries the current amps reading, so the responder can immediately see whether the compressor is running (equipment issue) or is off (power outage, breaker trip).
-
-## 9. Validation and Testing
-
-**Expected steady-state cadence.** A correctly-running cooler generates one `cooler_summary.qo` event approximately every `summary_interval_min` (default 60 minutes) and zero `cooler_alert.qo` events unless thresholds are breached. As a rough illustrative reference, a mid-size commercial walk-in cooler (800–1200 cu ft) at a typical QSR may show:
-- **Compressor runtime:** 35–55% duty cycle (21–33 minutes per 60-minute window)
-- **Compressor apparent kWh:** 0.3–0.6 kWh per window
-- **Door opens:** 8–20 per window (depends on traffic)
-- **Door open time:** 60–300 seconds cumulative per window
-- **Temperature drift:** 0–2 °F above setpoint (after stabilization)
-
-These are order-of-magnitude benchmarks; actual figures vary significantly with box volume, insulation, ambient conditions, product load, and defrost cycle frequency. `temp_f` consistently more than 3–4 °F above `temp_setpoint_f` is worth investigating regardless of runtime figures.
-
-**Simulating alerts during commissioning.** Two quick threshold tests: (a) set `temp_alert_f` to just below the current ambient in Notehub's environment-variable editor, wait one inbound sync (~120 minutes, or issue `hub.sync` via the blues.dev In-Browser Terminal), and observe a `temp_high` alert appear in the Events tab; (b) open the cooler door and leave it open past `door_open_alert_sec` to trigger `door_open_long`. Restore the original threshold after each test.
-
-**Using Mojo to validate power behavior.** The [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) sits inline between the 5V supply output and the Notecarrier CX +VBAT pin during bench testing and reports cumulative mAh over its [Qwiic](https://www.sparkfun.com/qwiic) I²C link. Because Mojo measures at the +VBAT rail, it sees the *whole assembly* — Notecard, Notecarrier CX, Cygnet host, DS18B20, CT bias circuit, and reed switch, not the Notecard alone.
-
-**(a) Published Notecard Cell+WiFi (MBGLW) current figures.** The table below cites figures from the [Blues low-power design documentation](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) and the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/). These are Notecard-only figures; the Mojo reading will be higher because it includes the Notecarrier and sensors.
-
-| Operating mode | Published current (Notecard only) | Source |
-|---|---|---|
-| Idle, radio off (between syncs) | ~8–18 µA @ 5V | [Blues low-power docs](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) |
-| LTE Cat-1 bis session active (primary RAT) | ~250 mA average; supply rail must handle brief current peaks. See [Blues cellular power-supply guidance](https://dev.blues.io/datasheets/application-notes/low-power-hardware-design/#power-supply-selection) for sizing | [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) |
-| GSM fallback (rare; areas without LTE coverage) | Up to ~2 A for a few milliseconds during GSM transmit burst; absorbed by the supply's output bulk capacitance within the IRM-10-5's 2 A continuous rating | [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) |
-| Per-session energy (single queued Note, bench test) | ~0.3 mAh | [Blues low-power design tests](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) |
-| WiFi active (opportunistic fallback) | Not independently specified in the MBGLW datasheet; the integrated Silicon Labs WFM200S WiFi module adds to the cellular idle baseline when active — consult the WFM200S transceiver datasheet for characterization data | [MBGLW datasheet — hardware overview](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) |
-
-**(b) Expected Mojo trace (whole assembly at +VBAT).** Your Mojo readings will exceed the Notecard-only floor because they include the Notecarrier CX regulator, the CT bias voltage divider (~165 µA continuous from the 3V3 rail), and connected-sensor quiescent draw. Expected pattern and approximate ranges by firmware phase:
-
-| Phase | Approx. whole-assembly current at +VBAT |
-|---|---|
-| Host off between samples (card.attn sleep, Notecard idle) | ~0.5–1 mA — well above the Notecard-only µA floor; CT bias divider and Notecarrier CX regulator quiescent dominate |
-| Host active during sampling (~1–2 seconds per wake) | ~15–30 mA spike — Cygnet STM32L4 active, DS18B20 750 milliseconds conversion, 2 × 150 milliseconds CT ADC passes |
-| Cellular outbound session — LTE Cat-1 bis (~30–60 seconds per hour) | ~250–300 mA — Notecard modem (~250 mA average) plus Notecarrier and sensor overhead; 5V/2A supply handles this with substantial headroom. In rare GSM-only coverage areas the modem bursts to ~2 A for a few ms; the supply's output capacitance absorbs this within its 2 A rating. |
-
-The *shape* of the trace is as informative as the absolute level. What you want to see: a stable sub-milliamp floor between wakes, a ~15–30 mA spike every 60 seconds lasting 1–2 seconds (one sample cycle), and a ~250–300 mA plateau once per summary window lasting tens of seconds (the LTE Cat-1 bis cellular session). A persistent multi-mA floor between wakes — above the ~0.5–1 mA sleep baseline — means the host is not fully sleeping, typically a `card.attn` wiring or firmware issue. Cellular plateaus more frequent than the configured summary cadence mean an alert is firing and triggering `sync:true` sessions outside the normal schedule. Both contributions are a negligible fraction of the 5V/2A supply's capacity; the bench measurement pays off most by catching firmware regressions that accidentally leave the host awake continuously.
-
-## 10. Troubleshooting
-
-**Device claims to Notehub but no events appear after 2+ hours.**
-- Check Notecard firmware is up to date: Notehub → Devices → click your device → Device Info → Notecard version. If outdated, trigger a [Notecard firmware update](https://dev.blues.io/notecard/notecard-walkthrough/updating-notecard-firmware/).
-- Confirm ProductUID in firmware matches your Notehub project. Notecard will not transmit to the wrong project.
-- In the [Blues In-Browser Terminal](https://dev.blues.io/terminal/) (top-right button), run `hub.status` to check cellular signal (`"signal": -100 to -50` dBm is typical). If `"status": "disconnected"`, wait 2–3 minutes and retry; initial connection can take multiple cycles.
-- Check that templates are registered: Events tab should show `cooler_summary.qo` and `cooler_alert.qo` Notefiles (even if empty). If missing, the `defineTemplates()` function failed—check firmware logs via USB serial with DEBUG_SERIAL enabled.
-
-**Events appear but temp_f or compressor_amps show NaN or -9999.**
-- `temp_f = -9999`: DS18B20 probe is disconnected, shorted, or the 4.7 kΩ pull-up is missing or open. Check D5 connectivity and probe 1-Wire bus.
-- `compressor_amps = 0.0`: CT is reading zero. Check that the CT clamp is around only one hot leg of the compressor circuit (not both, which cancels the field). Verify TRRS connector is fully seated at the SCT-013 input. Check A0 bias-circuit voltage is ~1.65 V (measure with a multimeter between A0 and GND).
-
-**Door-open alerts fire every sample even when door is closed.**
-- Door reed switch is stuck closed or the door-frame magnet is permanently magnetized. Confirm the switch reads LOW on D6 when door is physically closed and HIGH when open (check with a pin voltage meter or the [Blues In-Browser Terminal](https://dev.blues.io/terminal/) `card.attn` disabled, then upload a sketch that prints `digitalRead(D6)` to serial).
-- If magnet is stuck, replace the magnet.
-
-**Cellular sessions happen more frequently than expected (not just every `summary_interval_min`).**
-- Alerts are firing and triggering `sync:true` sessions outside the normal cadence. Check Events tab for `cooler_alert.qo` — if present, one of the two alert rules is active. Verify `temp_alert_f` and `door_open_alert_sec` thresholds are not too tight for your operating environment.
-
-**Device wakes and samples but the Mojo coulomb meter shows no change between sleep cycles.**
-- The host is not fully sleeping; `card.attn` may not be working. Verify I²C connectivity between Notecarrier and Notecard. Check firmware has no blocking delays or `delay()` calls outside of timeout-polled sensor reads (e.g., in `loop()` before `NotePayloadSaveAndSleep`).
-
-## 11. Limitations and Next Steps
-
-A reference design that has to drop onto 800 different walk-in boxes has to be deliberately modest about what it measures and what it concludes. The simplifications below are scope choices — places where a particular operator will want to add a sensor, calibrate a probe, or wire in a controller-side integration once the basic fleet visibility is paying for itself.
-
-### Simplified for this POC
-
-The simplifications below are scope choices — places where a particular operator will want to add a sensor, calibrate a probe, or wire in a controller-side integration once the basic fleet visibility is paying for itself.
-
-**Sensor reads are sample-based, not interrupt-driven.** All three sensors are polled once per `sample_interval_sec` (default 60 seconds). Any door opening, door closing, or compressor start/stop that occurs *and completes* within one 60-second sleep interval goes undetected. This quantizes edge timing to the sample period, which directly affects `door_opens`, `door_open_sec`, compressor runtime, and `kwh_window` — all of which can undercount if events are shorter than the sample period. `door_open_long` alert timing is similarly quantized: a door that opens just after one sample and closes just before the next may not trip the alert even if the physical open duration exceeded `door_open_alert_sec`. Reducing `sample_interval_sec` via environment variable improves resolution but increases host awake time and may increase alert frequency (more frequent checks mean alert conditions are detected sooner and `sync:true` sessions may be triggered more often). Note that scheduled outbound cellular sessions are still governed by `summary_interval_min` and do not increase with the sample interval alone.
-
-**AC supply voltage.** The MeanWell IRM-10-5 in the BOM accepts 85–264VAC wide-range input and handles both 120VAC and 208/240VAC single-phase — the majority of walk-in condensing-unit installations. Some mechanical rooms provide only a 24VAC or 24VDC control-transformer rail rather than mains voltage; those installs require a different converter (a 24VAC-input or 24VDC-input step-down regulator) rather than the AC/DC supply listed here. **Always confirm the available supply with the field electrician before ordering components.**
-
-**Compressor apparent energy only, not total cooler energy.** The single CT clamps one hot leg of the compressor circuit, so evaporator fans, defrost heaters, door heaters, controls, lighting, and other loads on the cooler's electrical service are not measured. `kwh_window` is a compressor-energy proxy, **not** total cooler or site energy consumption — a revenue-grade multifunction meter or additional CT channels for the non-compressor loads would be required if total box energy is the intended metric. In addition, the estimate uses apparent power (V × I) rather than real power (V × I × PF). For a typical single-phase hermetic compressor motor, power factor runs 0.75–0.90, so **the estimate will overstate actual compressor energy by 10–25 %.** A calibrated kilowatt-hour meter with a pulse output is the right solution for billing-grade energy monitoring; the single-CT apparent-power approach gives a useful *relative* metric for comparing compressor behavior across units and tracking trends over time, not an absolute kWh figure.
-
-**Single-phase only.** Larger walk-in boxes (over 4,000 sq ft) or walk-in freezers may run three-phase compressors. That requires three CTs, additional ADC channels, and firmware changes to sum the per-phase apparent power.
-
-**Temperature sensor without calibration offset.** DS18B20 probe accuracy is ±0.5 °C (±0.9 °F) factory-calibrated. For temperature-to-target deviation monitoring at ±1 °F precision, an in-situ calibration offset stored as an environment variable would eliminate unit-to-unit spread.
-
-**Temperature-to-target deviation is a downstream-derived metric, not an on-device rule — and the cooler controller's thermostat setpoint is not observed.** The device has no connection to the cooler's onboard controller or thermostat: `setpoint_f` in every summary Note is the corporate *target* temperature configured as a Notehub environment variable (`temp_setpoint_f`), not a value read from the cooler's electronic or mechanical controller. If actual controller-setpoint monitoring is required, integration with the cooler controller (via Modbus or a proprietary serial interface) would be needed. A downstream dashboard can compute deviation (`temp_f − setpoint_f`) per record, but the firmware itself never compares temperature against `setpoint_f`. The only on-device temperature alerting is the `temp_high` rule, which fires when `temp_f` exceeds the separate `temp_alert_f` threshold (default 40 °F). Because `temp_high` is a fixed threshold rather than a setpoint-relative check, **a legitimate spike during a freight delivery (door held open 10 minutes) triggers the same alert as a genuine refrigerant problem.** Gating the alert on compressor runtime and recent door activity would reduce false positives significantly.
-
-**No GNSS.** The Notecard Cell+WiFi includes a GNSS module, but this project does not use it because a fixed-location walk-in cooler box doesn't move. One-time site location can be bootstrapped from cellular tower triangulation via [`card.location`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location) if site lat/lon is needed for mapping dashboards.
-
-### Production Next Steps
-
-Once the basic fleet visibility is established, the following extensions are the natural progression — roughly from the most immediately useful to the most infrastructure-dependent.
-
-**A per-unit calibration offset for the DS18B20** (`temp_offset_f` env var, applied in `readBoxTempF()` at runtime) is the first refinement, tightening per-probe accuracy across the fleet.
-
-**A `door_open_max_sec` field on the summary payload** would capture the longest single uninterrupted door-open event within the window — useful for distinguishing a normal rapid-turnaround pattern from a single sustained-open incident, beyond what `door_open_sec` (total) alone conveys.
-
-**Three-phase compressor support** brings larger boxes into scope: three SCT-013-030 CTs on A0/A1/A2, RMS per leg, summed apparent power.
-
-**kWh power-factor correction** via a `power_factor` environment variable (0.80 default) lets ops dial in a per-unit-type correction after a baseline period against a reference meter, narrowing the apparent-vs-real-power gap noted above.
-
-**[Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** enables over-the-air firmware updates, so threshold-algorithm improvements can roll out fleet-wide without truck rolls.
-
-**A `cooler_config.qi` inbound Notefile** would allow ops-team-initiated diagnostic dumps — a full state snapshot on demand, without waiting for the next summary.
-
-## 12. Summary
-
-The corporate energy team that used to stall at the 50-store pilot now has a fleet-wide view: compressor apparent kWh per window, door-open seconds, temperature drift against a setpoint they configure from a browser. One cellular SKU, three sensors, and a field tech who never has to ask anyone for a WiFi password. And the outlier box running double the compressor-hours of its peers shows up on a dashboard instead of in next month's utility bill.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/multi-site-walk-in-cooler-energy-setpoint-monitor/)**
diff --git a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/banner.png b/54-multi-site-walk-in-cooler-energy-setpoint-monitor/banner.png
deleted file mode 100644
index da36c668..00000000
Binary files a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/banner.png and /dev/null differ
diff --git a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/01-system-architecture.svg b/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index 54305889..00000000
--- a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/02-wiring-assembly.svg b/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index b1bf7979..00000000
--- a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,89 +0,0 @@
-
diff --git a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/03-data-flow.svg b/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index ade4d3bb..00000000
--- a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,67 +0,0 @@
-
diff --git a/55-demand-response-solar-battery-dispatcher/README.md b/55-demand-response-solar-battery-dispatcher/README.md
index a48e0c47..b35151be 100644
--- a/55-demand-response-solar-battery-dispatcher/README.md
+++ b/55-demand-response-solar-battery-dispatcher/README.md
@@ -1,597 +1,5 @@
# Demand-Response Solar + Battery Dispatcher
-
+A reference design giving a commercial solar and battery installation an independent cellular control channel for peak-rate discharge, off-peak charging, and utility demand-response events.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Demand-Response%20Solar%20%2B%20Battery%20Dispatcher).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/solar_battery_dispatcher/](firmware/solar_battery_dispatcher/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is an [energy savings](https://blues.com/energy-savings/) reference design that gives a commercial solar + battery installation an independent cellular control channel, so the asset owner can dispatch the battery to discharge during expensive peak-rate hours, charge during cheap overnight hours, and curtail grid export when the utility calls a demand-response (DR) event, without depending on the building's IT network or a vendor's proprietary cloud portal. The device reads live operating state from the inverter and the battery's management system (BMS) over the industrial bus they already share, and signals each one to enter the right mode at the right time, driven by a schedule or by live commands routed through the [Blues Notehub](https://blues.com/notehub/) cloud service.
-
-**Scope.** This is a *mode enable / curtail* controller, not a power dispatcher. It closes and opens four dry-contact relays wired to digital control inputs the inverter and battery already expose; the field devices' own firmware then decides the actual charge, discharge, and export behavior — ramp rates, power limits, setpoints — based on their pre-commissioning configuration. The controller does not command power setpoints, verify that a requested dispatch profile was executed, or close any control loop around site load.
-
-## 1. Project Overview
-
-**The problem.** Commercial solar-plus-storage systems are a compelling economics story on paper: sell excess solar generation at peak rates, cover load from the battery so you don't draw from the grid during expensive peak windows, and participate in utility DR programs that pay you to curtail export or reduce load on short notice. The value is real, but realizing it requires a reliable, low-latency control channel between the utility signal and the physical system sitting on the roof or in the equipment yard.
-
-That's where most deployments hit the wall. The inverter and BMS speak Modbus, an industrial serial protocol developed in 1979 that remains the lingua franca of commercial power electronics. That bus sits in an outdoor equipment cabinet or a rooftop shed with no direct connection to the building's IT network. Even when WiFi does reach the equipment, the solar asset is often owned by a third-party power purchase agreement provider who has no credentials on the building's network and no relationship with the building's IT team. The control channel that does exist — the inverter's local display, a proprietary cloud portal, or a laptop plugged in at the cabinet — isn't a control channel you can reliably automate, audit, or integrate with a utility DR program.
-
-This project closes that gap. A [Blues Wireless for OPTA](https://blues.com/products/wireless-for-opta/) expansion snapped onto an Arduino OPTA RS485 PLC (programmable logic controller) sits on the DIN rail inside the equipment cabinet. The OPTA reads the inverter and BMS over the RS-485 Modbus bus already present in every commercial solar installation, while the Notecard inside the Blues expansion provides a cellular uplink that is completely independent of the building network and requires no IT coordination. When the utility issues a DR event, or when the local time-of-use peak window opens, the cloud-side system routes a dispatch command to the device through Notehub. The OPTA closes or opens four relay outputs: grid export enable, battery discharge, battery charge, and a DR-active indicator, typically within one to six minutes of the command arriving at Notehub (bounded by the 5-minute inbound sync cadence plus the next 1-minute sample cycle), then reports what it did and why.
-
-**Why Notecard.** The inverter and battery sit outdoors or in a rooftop equipment yard where WiFi is poor. More fundamentally, the solar asset is often owned by a PPA provider who doesn't have access to the building's network and can't request VPN credentials from a tenant's IT department. Cellular gives the asset owner an independent control channel that works identically whether the building is a strip mall, a warehouse, or a multi-tenant office park — no network forms, no AP to pair to, and no IT ticket to chase. That independence is the whole point: the asset owner needs to be able to send a discharge command at 4:57 PM regardless of what the building's network is doing.
-
-
-
-**Deployment scenario.** A single OPTA RS485 + Wireless for OPTA mounted on the DIN rail inside the solar equipment cabinet, next to the inverter's communication interface. The RS-485 bus daisies from the OPTA to the inverter and then to the BMS. Four relay output wires run to the corresponding digital control inputs on the inverter and BMS. The cellular antenna routes through a cable gland to the outside of the cabinet. Line power from the 24 VDC supply already present in the equipment cabinet. No PC, no gateway, no building LAN.
-
-
-
-**⚠ Commissioning safety — TOU windows are disabled by default.** Both the peak window (`peak_start_utc` / `peak_end_utc`) and the off-peak charge window (`charge_start_utc` / `charge_end_utc`) ship with start and end hours equal (both `0`), which disables autonomous TOU dispatch. The controller stays in `normal` mode until an operator explicitly configures non-equal window hours in the Notehub fleet environment variables. **Enable and tune the TOU windows only after confirming wiring, Modbus addressing, and SOC thresholds are correct for the site.** Cloud `dispatch.qi` commands are honored immediately regardless of TOU configuration. See [Section 5](#6-notehub-setup) for the full environment variable reference.
-
-
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Inside the equipment cabinet, the OPTA's Cortex-M7 plays Modbus RTU **client** (master) to the inverter and BMS — the Modbus **servers** / slaves — polling four holding registers from each across the onboard RS-485 transceiver once a minute. Every cycle the host figures out which operating mode applies — `normal`, `peak_discharge`, `overnight_charge`, `dr_curtail`, or `low_soc_protect` — checks live SOC against the configured guards, and drives the four relays accordingly. It also peeks at the `dispatch.qi` [inbound Notefile](https://dev.blues.io/notecard/notecard-walkthrough/inbound-requests-and-shared-data/) on every cycle for anything the cloud has queued. Outbound telemetry and mode-change events ride to the Notecard over I²C through the Wireless for OPTA expansion's AUX connector — no modem AT commands, no serial buffers, no session state machine.
-
-**[Notecard](https://shop.blues.com/products/notecard-cellular?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) responsibilities.** The Notecard queues the [Notes](https://dev.blues.io/api-reference/glossary/#note) (the JSON-based events), ships them on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `periodic` outbound cadence, and checks Notehub for inbound `dispatch.qi` commands every five minutes. That five-minute window is what keeps dispatch latency comfortably inside the ten-minute response envelope most utility DR programs require. The same module pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) on each inbound sync, which is how a fleet operator re-tunes the TOU schedule, SOC thresholds, or Modbus addresses from a browser without ever opening the cabinet.
-
-**Notehub responsibilities.** Events flow from the Notecard's embedded global SIM into [Notehub](https://notehub.io), which timestamps and stores every one and applies project-level [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). On the cloud side, whatever orchestrates dispatch — a utility rate scheduler, an energy management system, a webhook bridge to the utility DR API — queues commands by posting a Note to the device's `dispatch.qi` Notefile via the [Notehub API](https://dev.blues.io/api-reference/notehub-api/); the Notecard pulls it in on the next inbound sync. [Environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) set at the [Fleet](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) level carry the site's TOU schedule so the device can act autonomously — provided the Notecard has acquired a valid UTC time from at least one prior sync. On cold boot before that happens, `resolveMode()` sees `epoch = 0` and stays in `normal` rather than misapplying the schedule; TOU windows engage as soon as the clock is available.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-To compile and flash the firmware to an Arduino OPTA RS485:
-
-1. **Claim a Notehub project and copy its ProductUID.** Sign up at [notehub.io](https://notehub.io), create a new project, and copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid).
-
-2. **Edit the firmware.** Open `firmware/solar_battery_dispatcher/solar_battery_dispatcher.ino` and replace the placeholder `PRODUCT_UID` with your project's actual ProductUID:
- ```cpp
- #define PRODUCT_UID "com.example.mycompany:solar_battery_dispatcher"
- ```
-
-3. **Install dependencies via the Arduino IDE.** Install these from Boards Manager and Library Manager:
- - **Boards:** Arduino Mbed OS Opta Boards (search "opta")
- - **Libraries:** Blues Wireless Notecard, ArduinoModbus, ArduinoRS485
-
-4. **Compile and upload using arduino-cli (optional, for CI/headless builds):**
- ```bash
- arduino-cli core install "arduino:mbed_opta"
- arduino-cli lib install "Blues Wireless Notecard"
- arduino-cli lib install ArduinoModbus ArduinoRS485
- arduino-cli compile -b "arduino:mbed_opta:opta" firmware/solar_battery_dispatcher
- arduino-cli upload -b "arduino:mbed_opta:opta" -p /dev/ttyACM0 firmware/solar_battery_dispatcher
- ```
- (Replace `/dev/ttyACM0` with your OPTA's USB serial port, use `arduino-cli board list` to find it.)
-
-5. **Power the OPTA.** Provide 24 VDC to the board's power terminals. Within the first inbound sync window (up to 5 minutes), the Notecard automatically associates with your Notehub project.
-
-6. **Verify Notehub registration.** Open the [In-Browser Terminal](https://dev.blues.io/terminal/) and issue `card.status` to confirm `connected:true`, then `hub.status` to verify the project UID and at least one completed sync.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "solar_telemetry.qo",
- "body": {
- "pv_w": 4820.0,
- "ac_out_w": 2340.0,
- "grid_w": -680.0,
- "batt_soc_pct": 73.4,
- "batt_v": 51.2,
- "batt_a": 12.8,
- "mode": "peak_discharge"
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Arduino OPTA RS485](https://store.arduino.cc/products/opta-rs485) | 1 | Industrial PLC with onboard RS-485 transceiver, DIN-rail mount, 12–24 VDC supply, and four relay outputs. Required for the RS-485 interface; the OPTA Lite has no RS-485 and is not suitable. |
-| [Blues Wireless for OPTA (NA, SKU 992-00155-C)](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Snaps onto the OPTA's expansion port; adds a Notecard Cell+WiFi ([NOTE-WBNAW](https://dev.blues.io/datasheets/notecard-datasheet/note-wbnaw/)) over I²C. For EMEA deployments use [SKU 992-00156-C](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link). |
-| External cellular antenna(s) with SMA, ~3 m lead (e.g. [SparkFun CEL-16432](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html)) | 1 primary, 1 diversity recommended | Metal equipment cabinets kill rubber-duck antennas. Route at least the primary antenna outside through a cable gland; the diversity antenna improves LTE Cat-1 performance in marginal-signal sites. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Bench-only coulomb counter for validating the Wireless for OPTA + Notecard subsystem energy per session during commissioning. Not deployed to the field. |
-| 24 VDC DIN-rail supply, ≥15 W (e.g. [MeanWell HDR-15-24](https://www.meanwell.com/Upload/PDF/HDR-15/HDR-15-SPEC.PDF)) | 1 | Powers OPTA and expansion. Most solar equipment cabinets already have one; buy only if the cabinet doesn't. |
-| 120 Ω, ¼ W resistor | 2 | RS-485 line termination at each physical end of the Modbus bus. The OPTA's transceiver has no permanent terminator; floating the bus line causes spurious framing errors. |
-| Shielded twisted pair, 22 AWG, RS-485 rated | 1–3 m | A → A, B → B, shield → drive ground. Use a daisy-chain topology (not a star), shield and ground the cable per the inverter and BMS vendor installation guides, and terminate at both physical ends of the bus. Consult each vendor's documentation for maximum recommended bus length at your chosen baud rate. |
-| DIN rail, ~15 cm | 1 | Mount for OPTA + expansion. Likely already present in the equipment cabinet. |
-
-The Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-
-
-**Safety.** Solar equipment cabinets contain hazardous DC and AC voltages. Installation must be performed by qualified personnel following site lockout/tagout procedures, the inverter and BMS manufacturers' instructions, and applicable electrical codes. The relay outputs in this reference design provide contact closures to the inverter's and BMS's digital control inputs — they do not switch high-voltage circuits directly. Confirm input voltage and current ratings before wiring.
-
-
-
-### Power and DIN rail
-
-1. **Mount and power.** Snap the OPTA RS485 onto the DIN rail. Snap the Wireless for OPTA onto the OPTA's right-hand expansion port and connect the supplied solderless AUX connector between the two (this carries the I²C bus the Notecard rides on). Wire 24 VDC from the panel supply to the OPTA's `+` and `−` terminals. Per the [Wireless for OPTA Quickstart](https://dev.blues.io/quickstart/wireless-for-opta-quickstart/), jumper the OPTA's `+24V` to the expansion's `+24V` terminal so both devices share the same supply.
-
-### Antennas
-
-2. **Antennas.** Thread a male-SMA-to-female-bulkhead lead through a weatherproof cable gland in the cabinet and screw it onto the primary cellular antenna port on the Wireless for OPTA. Repeat for the diversity port. Do not leave the bundled rubber-duck antennas inside a metal cabinet — they are for bench testing only.
-
-### Modbus RS-485 bus
-
-3. **Modbus bus.** The OPTA is the Modbus client (master); the inverter and BMS are Modbus servers (slaves). Run shielded twisted-pair from the OPTA's RS-485 terminals to the inverter's communication port, then daisy-chain from the inverter to the BMS:
- - OPTA `A (+)` → inverter `A (+)` → BMS `A (+)`
- - OPTA `B (−)` → inverter `B (−)` → BMS `B (−)`
- - OPTA `COM` → inverter communication ground → BMS communication ground
- - Place a 120 Ω termination resistor across `A` / `B` at the OPTA end and another at the far end of the chain (the BMS, if the inverter is in the middle). This is a three-node daisy-chain (OPTA → inverter → BMS) with terminators only at its two physical ends.
-
-4. **Modbus device configuration.** Configure the inverter as Modbus slave ID `1` (default `modbus_slave_inv`), the BMS as slave ID `2` (default `modbus_slave_bms`). Use the `modbus_baud`, `modbus_parity`, and `modbus_stop_bits` environment variables to match the serial configuration required by your hardware (firmware default: 9600 baud, no parity, 1 stop bit, 9600 8N1). Consult the Modbus communication or RS-485 configuration section in the inverter and BMS vendor manuals to identify the correct slave IDs and serial settings for each device.
-
-
-
- **Hard constraint — single shared bus.** The firmware uses one RS-485 physical segment with **one set of serial parameters** (`modbus_baud`, `modbus_parity`, `modbus_stop_bits`) applied to both the inverter and the BMS. Both devices must be configured to identical baud rate, parity, and stop bits — they are polled sequentially on the same electrical bus. This is a commissioning prerequisite, not just a configuration choice: if your inverter and BMS ship with different default serial settings and cannot both be reconfigured to a common setting, this reference topology is unworkable as-is. In that case you would need a second RS-485 port or a USB-to-RS-485 adapter on a second UART, plus matching firmware changes to address each device on its own serial port. Verify serial-setting compatibility between your chosen inverter and BMS before committing to this wiring layout.
-
-
-
-### Relay output wiring
-
-5. **Relay outputs.** The OPTA RS485 provides four electromechanical relay outputs. Each relay exposes two field-wiring terminals: **COM** (common) and **NO** (Normally Open, open when the relay is de-energized, closed when energized). There is no NC terminal on the OPTA RS485 terminal block. The relay contacts are volt-free (dry); they provide an isolated switch only — no voltage is driven onto the field wiring by the relay itself. The field device's own internal supply provides the reference voltage for its digital input; the relay contact simply completes or breaks that circuit. Consult the [Arduino OPTA RS485 hardware documentation](https://docs.arduino.cc/hardware/opta/) for the exact terminal block pin assignments and relay contact ratings before wiring.
-
- **Fail-safe behavior.** When the OPTA de-energizes on power loss, hardware fault, or startup, all NO contacts open and every controlled function falls to its safe default state. Wire all four relay paths using COM and NO as shown in the table below.
-
- **How to wire a dry-contact digital input through the relay.** Most inverter and BMS digital inputs designed for dry-contact operation provide their own excitation voltage internally. Two wires from the relay's COM and NO terminals connect directly to the two designated dry-contact terminals on the field device (polarity is irrelevant, the relay is just a switch). When the relay energizes, COM and NO close, completing the circuit and asserting the input. When the relay de-energizes, the circuit opens and the input de-asserts. Verify the input terminal designations, drive voltage, and maximum contact current against the field device's installation manual before wiring.
-
- | Relay | OPTA contacts to use | Mapped function | De-energized (open) fail state |
- |-------|---------------------|-----------------|-------------------------------|
- | `RELAY1` | `COM` + `NO` | Inverter *Grid Export Enable* | Export curtailed (conservative fail-safe design choice) — verify the required fail state against the inverter manual, site protection scheme, and utility interconnect agreement before commissioning |
- | `RELAY2` | `COM` + `NO` | BMS *Battery Discharge Enable* | Discharge blocked — battery protected against uncontrolled discharge during fault |
- | `RELAY3` | `COM` + `NO` | BMS *Battery Charge Enable* | Charge inhibited — BMS own protection circuits remain responsible; charge halts cleanly |
- | `RELAY4` | `COM` + `NO` | SCADA / indicator lamp *DR Active* | DR indicator off — no false active signal during startup or fault |
-
- **Per-path field wiring:**
- - **RELAY1 (Grid Export Enable):** Run two wires from the RELAY1 `COM` and `NO` terminals to the inverter's Grid Export Enable dry-contact input terminals. The firmware asserts this relay HIGH in `normal`, `peak_discharge`, `overnight_charge`, and `low_soc_protect` modes; it opens (de-energizes) only in `dr_curtail` mode to curtail export per the utility DR instruction.
- - **RELAY2 (Battery Discharge Enable):** Run two wires from RELAY2 `COM` and `NO` to the BMS's Discharge Enable dry-contact input. The relay closes only when `peak_discharge` is the active mode and SOC is above `soc_min_pct + soc_hyst_pct`; it opens in every other mode and whenever the SOC guard latches.
- - **RELAY3 (Battery Charge Enable):** Run two wires from RELAY3 `COM` and `NO` to the BMS's Charge Enable dry-contact input. The relay closes in `normal`, `overnight_charge`, and `low_soc_protect` modes when the upper-SOC latch is not active; it opens once SOC reaches `soc_max_pct` and does not re-close until SOC drops to `soc_max_pct − soc_max_hyst_pct` (default 92 %). This deadband prevents charge-relay chatter when BMS readings oscillate near the charge ceiling between consecutive sample cycles. The relay also opens unconditionally during `peak_discharge` (never charge while discharging) and `dr_curtail` (follow DR instructions).
- - **RELAY4 (DR Active indicator):** Run two wires from RELAY4 `COM` and `NO` to the SCADA dry-contact input or indicator lamp. The relay closes only in `dr_curtail` mode. If driving a resistive or LED lamp load, confirm the lamp's rated current is within the relay's contact current rating per the OPTA datasheet.
-
-### Bench power validation
-
-6. **Mojo placement.** During commissioning, splice the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the 24 VDC supply and the Wireless for OPTA power input to measure the expansion + Notecard subsystem energy per cellular session. See [Section 9](#9-validation-and-testing) for the expected figures.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/solar_battery_dispatcher/solar_battery_dispatcher.ino` as `PRODUCT_UID`.
-
-2. **Claim the Notecard.** Power the panel supply; on first cellular session the Notecard associates with your Notehub project automatically.
-
-3. **Create a Fleet per site.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. The natural unit here is one fleet per installation site — inverters at the same site operate under the same TOU schedule and the same SOC thresholds, so those values belong at the fleet level. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can further subdivide a fleet by inverter capacity class or PPA contract type.
-
-4. **Set environment variables.** All variables are optional; firmware defaults are shown. The Notecard picks up variable changes from Notehub on its next inbound sync and caches them on-device; the firmware reads and applies them at the start of the next report cycle (`fetchEnvOverrides()` is called once per `report_minutes`). A change to `report_minutes` also causes the firmware to re-issue `hub.set` during that same cycle, updating the Notecard's outbound sync cadence without a reboot. Operators can push a new TOU schedule or tighten the minimum SOC without touching firmware.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_minutes` | `1` | Modbus poll cadence (minutes). **DR dispatch latency is `inbound cadence (5 min) + sample_minutes`.** At the default of `1`, end-to-end relay response is at most 6 minutes. Increasing `sample_minutes` directly increases dispatch latency by the same amount — a value of `5` keeps worst-case latency at exactly 10 minutes, the outer boundary of most utility DR response envelopes. The firmware clamps this variable to the range **1–5** to enforce this bound. |
- | `report_minutes` | `15` | Telemetry Note cadence (minutes). |
- | `soc_min_pct` | `20.0` | Minimum SOC (%) below which battery discharge is blocked, regardless of dispatch mode. |
- | `soc_max_pct` | `95.0` | Maximum SOC (%) above which battery charging is blocked. |
- | `soc_hyst_pct` | `5.0` | SOC hysteresis band (%) above `soc_min_pct`. Once `low_soc_protect` latches, discharge does not resume until SOC rises to `soc_min_pct + soc_hyst_pct` (e.g., 25 % with the default 20 % minimum). Prevents relay chatter and repeated `dr_event.qo` traffic when SOC readings oscillate near the lower threshold between consecutive 1-minute sample cycles. Range: 0–20. |
- | `soc_max_hyst_pct` | `3.0` | SOC hysteresis band (%) below `soc_max_pct`. Once charge-inhibit latches (SOC reaches `soc_max_pct`), charging does not resume until SOC drops to `soc_max_pct − soc_max_hyst_pct` (e.g., 92 % with the default 95 % ceiling and 3 % band). Without this deadband a noisy SOC reading oscillating near the charge ceiling would toggle RELAY3 on every 1-minute sample cycle. The charge-inhibit latch operates inside `applyRelays()` without changing the resolved mode, so it does not generate `dr_event.qo` events — its effect is relay anti-chatter only. Range: 0–20. |
- | `peak_start_utc` | `0` | Start hour of the TOU peak window (UTC, 0–23). Defaults to `0` (window disabled, start equals end). Configure a non-equal pair (e.g. `peak_start_utc=21`, `peak_end_utc=2`) to enable autonomous peak discharge during that window. |
- | `peak_end_utc` | `0` | End hour of the TOU peak window (UTC, 0–23). Wrap-around supported (e.g. 21 → 02 spans midnight). When `peak_start_utc` equals `peak_end_utc`, the peak window is disabled — the same equal-values rule that applies to the charge window. |
- | `charge_start_utc` | `0` | Start hour of the off-peak charge window (UTC, 0–23). When `charge_start_utc` equals `charge_end_utc` (the default), autonomous overnight charging is disabled and `overnight_charge` is available only via cloud dispatch. |
- | `charge_end_utc` | `0` | End hour of the off-peak charge window (UTC, 0–23). Wrap-around supported. Set a nonzero window (e.g. `charge_start_utc=0, charge_end_utc=6`) to have the TOU schedule enter `overnight_charge` autonomously during off-peak hours. |
- | `modbus_slave_inv` | `1` | Modbus slave ID for the inverter. |
- | `modbus_slave_bms` | `2` | Modbus slave ID for the battery BMS. |
- | `modbus_baud` | `9600` | RS-485 baud rate for both devices (must match hardware configuration). |
- | `modbus_parity` | `0` | RS-485 parity: `0` = none (N), `1` = even (E), `2` = odd (O). Must match hardware configuration. |
- | `modbus_stop_bits` | `1` | RS-485 stop bits: `1` or `2`. Must match hardware configuration. |
- | `reg_inv_base` | `100` | Starting holding-register address (0-based, wire-level) for the inverter block. |
- | `reg_bms_base` | `200` | Starting holding-register address for the BMS block. |
-
-5. **Configure routes.** Add a route for `solar_telemetry.qo` to a long-term analytics or historian destination (this Note is relatively high frequency, 96 records per device per day at the 15-minute default) and a separate route for `dr_event.qo` to a real-time notification channel such as a CMMS, SCADA system, or operator dashboard. Because the two Notefiles are separate at the source, each can be routed differently with no filter logic in the route itself.
-
-6. **Inbound dispatch path.** Cloud-side systems send dispatch commands by posting a Note to the device's `dispatch.qi` Notefile via the [Notehub API](https://dev.blues.io/api-reference/notehub-api/) (`POST /v1/projects/{projectUID}/devices/{deviceUID}/notes/dispatch.qi`). The Note body carries a `mode` string and an optional `expires_epoch` Unix timestamp. The Notecard delivers the Note to the device within the next inbound sync window (≤5 minutes).
-
- | `mode` value | Effect |
- |---|---|
- | `peak_discharge` | Immediately enables battery discharge and grid export, overriding the TOU schedule. |
- | `overnight_charge` | Immediately enables grid-charging the battery, overriding the TOU schedule. |
- | `dr_curtail` | Curtails grid export and asserts the DR-indicator relay. |
- | `normal` | **TOU-suppressing override.** Clears any active `peak_discharge`, `overnight_charge`, or `dr_curtail` command and holds the system in normal relay state — suppressing autonomous TOU transitions — for the duration of any `expires_epoch` window (or until superseded by another command). Dispatching `mode:"normal"` is not a no-op: it is the correct mechanism for forcibly returning the controller to normal operation during an active TOU peak or DR window from the cloud. On the wire (telemetry and events) this state is reported as `"normal"`. Because both the idle `MODE_NORMAL` and the active `MODE_FORCED_NORMAL` serialize to `"normal"`, dispatching `mode:"normal"` while the system is already in normal state (no active TOU window) does not produce a `dr_event.qo` — events are only emitted when the wire-visible name changes. |
-
- When `expires_epoch` is set, the device automatically reverts to its TOU-scheduled mode (which may be `peak_discharge`, `overnight_charge`, or `normal` depending on the current UTC hour) when the epoch arrives — providing a built-in recovery if the cloud system fails to send an explicit release command.
-
- **Posting a dispatch command via curl:** Replace `{projectUID}`, `{deviceUID}`, and `{apiToken}` with your actual Notehub credentials (create an API token in Notehub → Settings → API keys):
-
- ```bash
- curl -X POST https://api.notefile.io/v1/projects/{projectUID}/devices/{deviceUID}/notes/dispatch.qi \
- -H "Authorization: Bearer {apiToken}" \
- -H "Content-Type: application/json" \
- -d '{
- "body": {
- "mode": "dr_curtail",
- "expires_epoch": '$(( $(date +%s) + 600 ))'
- }
- }'
- ```
-
- This example issues a DR curtailment that automatically expires in 10 minutes. The device will respond with a `dr_event.qo` Note and close RELAY4 (DR indicator) and open RELAY1 (grid export) on the next sample cycle.
-
-## 7. Firmware Design
-
-Five files in [`firmware/solar_battery_dispatcher/`](firmware/solar_battery_dispatcher/):
-
-| File | Role |
-|------|------|
-| [`solar_battery_dispatcher.ino`](firmware/solar_battery_dispatcher/solar_battery_dispatcher.ino) | Entry point: `PRODUCT_UID` define (**edit this**), `setup()`, `loop()`, and all global variable definitions |
-| [`dispatcher.h`](firmware/solar_battery_dispatcher/dispatcher.h) | Shared types (`DispatchMode`, `InverterSample`, `BmsSample`), relay-pin defines, `extern` declarations for module-global variables, and function prototypes used by all compilation units |
-| [`notecard_helpers.cpp`](firmware/solar_battery_dispatcher/notecard_helpers.cpp) | Notecard configuration (`hub.set`, templates), environment-variable fetch and validation (`fetchEnvOverrides`), outbound sync-cadence management, and inbound dispatch-Note polling (`checkDispatch`) |
-| [`modbus_helpers.cpp`](firmware/solar_battery_dispatcher/modbus_helpers.cpp) | Modbus RTU bus initialization (`applyModbusIfChanged`) and device polling (`pollInverter`, `pollBms`) |
-| [`mode_helpers.cpp`](firmware/solar_battery_dispatcher/mode_helpers.cpp) | UTC time utilities, mode resolution (`resolveMode`), relay output control (`applyRelays`), and outbound Note emission (`sendTelemetry`, `sendModeEvent`) |
-
-Dependencies:
-- Arduino Mbed OS Opta Boards core (install via the Arduino IDE Boards Manager, search "opta").
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (`note-arduino` library). Install via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`. See [note-arduino releases](https://github.com/blues/note-arduino/releases) for the latest.
-- [`ArduinoModbus`](https://github.com/arduino-libraries/ArduinoModbus) and [`ArduinoRS485`](https://github.com/arduino-libraries/ArduinoRS485) — official Arduino libraries, install via Library Manager.
-
-### Modules
-
-| Responsibility | Function | File |
-|---|---|---|
-| Notecard configuration (`hub.set`, templates) | `notecardConfigure`, `defineTemplates` | `notecard_helpers.cpp` |
-| Environment-variable fetch + clamp | `fetchEnvOverrides` | `notecard_helpers.cpp` |
-| Notecard outbound cadence update | `applyHubSetIfChanged` | `notecard_helpers.cpp` |
-| Modbus serial config update | `applyModbusIfChanged` | `modbus_helpers.cpp` |
-| Modbus polling | `pollInverter`, `pollBms` | `modbus_helpers.cpp` |
-| Inbound dispatch Note consumption | `checkDispatch` | `notecard_helpers.cpp` |
-| UTC time query with extrapolation | `currentUtcEpoch` | `mode_helpers.cpp` |
-| Mode resolution (cloud dispatch + TOU schedule + lower-SOC guard) | `resolveMode` | `mode_helpers.cpp` |
-| Relay output control (upper-SOC latch; fail-safe on BMS comm loss) | `applyRelays` | `mode_helpers.cpp` |
-| Outbound telemetry emission | `sendTelemetry` | `mode_helpers.cpp` |
-| Immediate mode-change event | `sendModeEvent` | `mode_helpers.cpp` |
-
-### Sensor reading strategy
-
-The inverter and BMS are each polled with a four-register `requestFrom` block — one Modbus transaction per device. The inverter block (at `reg_inv_base`) yields PV generation, AC output to load, grid exchange power (negative = importing, positive = exporting), and inverter state. The BMS block (at `reg_bms_base`) yields SOC percentage, battery voltage, battery current (negative = discharging), and BMS state. The demo register map is contiguous and uses simple integer scaling (see the firmware comments for exact scaling); real inverters and BMS units have vendor-specific maps. See [Limitations](#11-limitations-and-next-steps).
-
-Each device allows up to three retries per poll cycle. If all three fail, `valid` is left `false` for that device. The telemetry Note reflects the most recent sample at the time of reporting: if the poll that immediately preceded the report boundary failed (leaving the sample invalid), the `-9999` sentinel is emitted for all fields of that device so downstream analytics can distinguish "no reading" from a real zero or negative value; a transient failure that resolves before the next reporting boundary will not appear as `-9999` in that report. Mode decisions during a Modbus outage fall back to the last dispatched command or the TOU schedule — the device doesn't stall waiting for bus recovery.
-
-### Event payload design
-
-Two Notefiles, with different urgencies and destinations.
-
-`solar_telemetry.qo` is [template-encoded](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) and transmitted on the `report_minutes` cadence (default 15 minutes). Templates compress these Notes from free-form JSON into fixed-length records — at 96 records per day across a multi-year deployment on a prepaid SIM, the 3–5× bandwidth savings is material. Example body when all devices are reachable:
-
-```json
-{
- "file": "solar_telemetry.qo",
- "body": {
- "pv_w": 4820.0,
- "ac_out_w": 2340.0,
- "grid_w": -680.0,
- "batt_soc_pct": 73.4,
- "batt_v": 51.2,
- "batt_a": 12.8,
- "mode": "peak_discharge"
- }
-}
-```
-
-When a Modbus device becomes unreachable (inverter or BMS), all fields for that device are replaced with the sentinel value `-9999` until communications recover. For example, if the BMS is unreachable:
-
-```json
-{
- "file": "solar_telemetry.qo",
- "body": {
- "pv_w": 4820.0,
- "ac_out_w": 2340.0,
- "grid_w": -680.0,
- "batt_soc_pct": -9999.0,
- "batt_v": -9999.0,
- "batt_a": -9999.0,
- "mode": "low_soc_protect"
- }
-}
-```
-
-Downstream consumers can filter on `-9999` to detect and alert on communication loss.
-
-`dr_event.qo` is a Note emitted with `sync:true` on every mode transition — when `peak_discharge` kicks in at the start of the TOU peak window, or when a DR curtailment arrives from the cloud, or when a command expires and the system returns to normal. The immediate uplink ensures operators and downstream systems see the transition in under a minute. Example:
-
-```json
-{
- "file": "dr_event.qo",
- "body": {
- "new_mode": "dr_curtail",
- "prev_mode": "normal",
- "batt_soc_pct": 67.5,
- "pv_w": 3100.0,
- "grid_w": 420.0
- },
- "sync": true
-}
-```
-
-### Power and sync strategy
-
-The OPTA + expansion is line-powered (24 VDC), so MCU deep sleep is not the goal — bus efficiency and inbound latency are. The Notecard runs in [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `periodic` mode with `outbound` equal to `report_minutes` (default 15 minutes) and `inbound` fixed at 5 minutes. If `report_minutes` is updated via a Notehub environment variable, the firmware re-issues `hub.set` at the following report boundary (`applyHubSetIfChanged()`), so the Notecard's outbound cadence stays aligned with the emission interval without requiring a reboot. Because `inbound` is the shorter interval, the Notecard wakes the radio every 5 minutes to check for inbound dispatch commands and pick up environment variable updates. Every third inbound session — at the 15-minute `outbound` boundary — also flushes the outbound queue, shipping accumulated telemetry Notes in one batch. Mode-change events use `sync:true` to bypass the outbound timer and ship within a session-establishment window of the trigger, typically 15–60 seconds. Between sessions the Notecard's idle draw is ~8–18 µA at 5 V.
-
-Sampling and transmission are deliberately decoupled: the firmware polls Modbus every minute but only sends a telemetry Note every 15 minutes. This keeps transmission sessions short and predictable while still giving the mode-resolution logic fresh SOC and power readings every 60 seconds.
-
-### Retry and error handling
-
-- The first Notecard transaction uses `notecard.sendRequestWithRetry(req, 5)` to paper over the known cold-boot I²C race condition at power-up. The applied outbound cadence is recorded only on a successful return; if the call fails, `g_last_report_minutes` stays at its sentinel (`0`) and `applyHubSetIfChanged()` retries `hub.set` from the main loop on every report boundary until it succeeds.
-- Modbus reads are retried up to three times per poll cycle with 100 milliseconds between attempts. A failed poll logs to the serial debug port (and the `-9999` sentinel appears in the telemetry Note if the failure was still present at the next report boundary. See Sensor reading strategy above). BMS comm loss triggers two independent fail-safe responses: if `peak_discharge` is the active mode, `resolveMode()` returns `MODE_LOW_SOC_PROTECT` and the discharge relay opens (the device cannot confirm battery state, so it protects the battery and emits a `dr_event.qo` mode-change event); in every mode, `applyRelays()` forces the upper-SOC charge-inhibit latch active, preventing RELAY3 from closing until BMS comms recover and a live SOC reading confirms the battery has cleared the charge ceiling. Both guards fail safe symmetrically — battery comm loss blocks both discharge and charge.
-- `fetchEnvOverrides()` clamps every environment variable to a safe range before applying it — a pathological value (e.g., `sample_minutes = 0`) cannot create a tight loop or an invalid Modbus slave address.
-- Dispatch commands that include an `expires_epoch` automatically revert when the epoch arrives, protecting against a stuck DR state if the cloud system fails to send a recovery command. `currentUtcEpoch()` extrapolates from the last known-good epoch through any transient failure — it never returns `0` after time has been acquired at least once, so expiry and TOU scheduling remain accurate during brief Notecard API or connectivity disruptions.
-- All `requestAndResponse()` calls check both `NULL` return and the `err` field on the response before trusting the data.
-
-### Key code snippet 1: inbound dispatch polling
-
-The firmware calls `checkDispatch()` every sample cycle, popping the `dispatch.qi` queue and translating the `mode` string into an internal enum:
-
-```cpp
-J *req = notecard.newRequest("note.get");
-JAddStringToObject(req, "file", "dispatch.qi");
-JAddBoolToObject (req, "delete", true); // pop — consumed once
-
-J *rsp = notecard.requestAndResponse(req);
-if (!notecard.responseError(rsp)) {
- J *body = JGetObject(rsp, "body");
- const char *mode_str = JGetString(body, "mode");
- double expires = JGetNumber(body, "expires_epoch");
-
- // Resolve the mode string first; only commit mode and expiry together
- // once the mode is confirmed valid — an unrecognised mode must not alter
- // g_dr_expires_epoch and corrupt the lifetime of the current valid command.
- DispatchMode new_cmd = g_commanded_mode;
- bool valid = true;
- if (strcasecmp(mode_str, "dr_curtail") == 0) new_cmd = MODE_DR_CURTAIL;
- else if (strcasecmp(mode_str, "peak_discharge") == 0) new_cmd = MODE_PEAK_DISCHARGE;
- else if (strcasecmp(mode_str, "overnight_charge") == 0) new_cmd = MODE_OVERNIGHT_CHARGE;
- // "normal" maps to the internal MODE_FORCED_NORMAL — a distinct value that
- // is != MODE_NORMAL, so it actively suppresses TOU evaluation for the
- // duration of any expires_epoch window rather than silently releasing back
- // to the schedule while a peak or charge window is still active.
- // On the wire it is reported as "normal" (see modeName() in the sketch).
- else if (strcasecmp(mode_str, "normal") == 0) new_cmd = MODE_FORCED_NORMAL;
- else { valid = false; } // unknown mode — log, leave existing command untouched
-
- if (valid) {
- g_commanded_mode = new_cmd;
- g_dr_expires_epoch = (expires > 0) ? (uint32_t)expires : 0;
- }
-}
-notecard.deleteResponse(rsp);
-```
-
-### Key code snippet 2: mode resolution with TOU schedule and SOC guard
-
-`resolveMode()` layers cloud dispatch, TOU schedule, and lower-SOC protection. Three design points worth noting:
-
-1. **TOU suppression.** `g_commanded_mode` is the only input that suppresses TOU evaluation. Any value other than `MODE_NORMAL`, including `MODE_FORCED_NORMAL` (stored when the cloud dispatches `mode:"normal"`) — causes the function to skip the TOU window checks. Because `MODE_FORCED_NORMAL != MODE_NORMAL`, a cloud `mode:"normal"` command genuinely holds the system in normal operation rather than silently releasing back to a TOU peak-discharge window that may still be active. The resolved active mode from the previous cycle is never fed back, so TOU transitions, dispatch expiry, and SOC-guard recovery all happen cleanly on every poll cycle. **Because `MODE_FORCED_NORMAL` and `MODE_NORMAL` both serialize to `"normal"` via `modeName()`, mode-change events in `loop()` are gated on `strcmp(modeName(new_mode), modeName(s_prev_mode)) != 0` rather than a raw enum comparison.** This prevents a no-op `dr_event.qo` with `prev_mode:"normal"` / `new_mode:"normal"` when a forced-normal command arrives while the system is already in normal state, or when it expires back to normal. `s_prev_mode` is updated unconditionally each cycle so the comparison always reflects the current resolved state.
-
-2. **Lower SOC guard.** `MODE_LOW_SOC_PROTECT` is returned, and appears in telemetry and events as `"low_soc_protect"` — whenever SOC is critically low or the BMS is unreachable while `peak_discharge` is active. The guard uses a **hysteresis latch** (`s_in_soc_protect`): it engages when SOC drops below `soc_min_pct` and releases only after SOC recovers to `soc_min_pct + soc_hyst_pct` (default +5 %). Without the hysteresis band a noisy SOC reading oscillating near the threshold would flip the discharge relay and emit a `dr_event.qo` mode-change Note on every 1-minute sample cycle.
-
-3. **Upper SOC guard (charge inhibit).** `applyRelays()` uses a matching deadband latch (`s_in_charge_inhibit`, private to `mode_helpers.cpp`) for the charge path: it engages when SOC reaches `soc_max_pct` and releases only after SOC drops to `soc_max_pct − soc_max_hyst_pct` (default 92 %). Without this band, a noisy SOC reading oscillating near the charge ceiling would toggle RELAY3 on every 1-minute sample cycle. Unlike the lower-SOC guard, the charge-inhibit latch operates entirely inside `applyRelays()` without changing the resolved mode; it therefore does not affect the wire-name mode-change comparison in `loop()` and does not generate `dr_event.qo` events — its effect is relay anti-chatter only. **BMS comm loss is also treated as charge-inhibit:** when `bms_valid` is false, the latch is forced active regardless of the sentinel SOC value — charging is blocked because we cannot confirm the battery has cleared the ceiling. This mirrors the lower-guard's discharge fail-safe, making both ends of the SOC operating range symmetric:
-
-```cpp
-static DispatchMode resolveMode(uint32_t utc_epoch, bool bms_valid, float soc_pct) {
- // Expire a timed dispatch; device falls back to the TOU schedule automatically.
- if (g_dr_expires_epoch > 0 && utc_epoch >= g_dr_expires_epoch) {
- g_commanded_mode = MODE_NORMAL;
- g_dr_expires_epoch = 0;
- }
- // Determine candidate: cloud dispatch > TOU peak > TOU off-peak charge > normal.
- // Only g_commanded_mode suppresses TOU evaluation; g_active_mode is never fed
- // back, so TOU transitions and SOC-guard recovery happen cleanly each cycle.
- DispatchMode candidate = MODE_NORMAL;
- if (g_commanded_mode != MODE_NORMAL) {
- candidate = g_commanded_mode; // active cloud dispatch overrides schedule
- } else if (utc_epoch > 0 && isInPeakWindow(utcHour(utc_epoch), g_peak_start_utc, g_peak_end_utc)) {
- candidate = MODE_PEAK_DISCHARGE;
- } else if (utc_epoch > 0 && isInPeakWindow(utcHour(utc_epoch), g_charge_start_utc, g_charge_end_utc)) {
- candidate = MODE_OVERNIGHT_CHARGE;
- }
- // SOC guard with hysteresis: latch into low_soc_protect at soc_min_pct; release
- // only after SOC recovers to soc_min_pct + soc_hyst_pct (default +5 %). Prevents
- // relay chatter on noisy SOC readings near the threshold between consecutive cycles.
- // Also triggers on BMS loss (bms_valid == false): fail safe — cannot confirm
- // battery state, so discharge is not permitted.
- // DR_CURTAIL restricts export only and does not enable discharge, so it is
- // excluded from the would_discharge check.
- bool would_discharge = (candidate == MODE_PEAK_DISCHARGE);
- if (would_discharge) {
- if (!bms_valid || soc_pct < g_soc_min_pct) {
- s_in_soc_protect = true;
- } else if (s_in_soc_protect && soc_pct >= (g_soc_min_pct + g_soc_hyst_pct)) {
- s_in_soc_protect = false;
- }
- if (s_in_soc_protect) return MODE_LOW_SOC_PROTECT;
- } else {
- s_in_soc_protect = false; // not discharging — clear latch for next entry
- }
- return candidate;
-}
-```
-
-### Key code snippet 3: relay control
-
-`applyRelays()` translates the resolved mode into four relay states. The primary lower-SOC guard lives in `resolveMode()` (which returns `MODE_LOW_SOC_PROTECT` when needed); `applyRelays()` retains a belt-and-suspenders discharge check for `peak_discharge`. The upper-SOC deadband latch controls `charge_en` for all charging modes. `dr_curtail` opens the export relay and asserts the DR-indicator relay only — battery discharge is not forced during a DR event. BMS comm loss (`bms_valid == false`) forces the charge-inhibit latch active, mirroring the discharge fail-safe in `resolveMode()`:
-
-```cpp
-void applyRelays(DispatchMode mode, float soc_pct, bool bms_valid) {
- bool low_soc = (soc_pct < g_soc_min_pct);
-
- // Upper-SOC charge-inhibit latch with BMS comm-loss fail-safe.
- // On BMS comm loss the latch is forced active — we cannot confirm the
- // battery has cleared the charge ceiling, so charging is blocked.
- // The latch releases only once BMS comms are restored and a live SOC
- // reading confirms the battery has dropped through the hysteresis band.
- if (!bms_valid) {
- s_in_charge_inhibit = true;
- } else if (soc_pct >= g_soc_max_pct) {
- s_in_charge_inhibit = true; // engage: SOC at or above ceiling
- } else if (s_in_charge_inhibit && soc_pct <= (g_soc_max_pct - g_soc_max_hyst_pct)) {
- s_in_charge_inhibit = false; // release: SOC dropped through deadband
- }
- bool full_soc = s_in_charge_inhibit;
-
- switch (mode) {
- case MODE_NORMAL:
- export_en = true; discharge_en = false; charge_en = !full_soc; break;
- case MODE_OVERNIGHT_CHARGE:
- export_en = true; discharge_en = false; charge_en = !full_soc; break;
- case MODE_DR_CURTAIL:
- export_en = false; discharge_en = false; charge_en = false; dr_active = true; break;
- // ... other modes
- }
- if (low_soc) discharge_en = false; // belt-and-suspenders lower guard
-
- digitalWrite(RELAY_BATT_CHARGE, charge_en ? HIGH : LOW);
- digitalWrite(RELAY_BATT_DISCHARGE, discharge_en ? HIGH : LOW);
- digitalWrite(RELAY_GRID_EXPORT, export_en ? HIGH : LOW);
- // ...
-}
-```
-
-## 8. Data Flow
-
-
-
-**Collected.** Every `sample_minutes` (default 1 minutes): PV generation in watts, AC output to load in watts, grid exchange power in watts, battery SOC in percent, battery voltage in volts, battery current in amps, and inverter/BMS state codes (`inv_state`, `bms_state`). The state codes are read from Modbus and stored in the sample structs but are **not included in outbound telemetry or mode-change events** in this reference design — they are placeholder fields for future vendor-specific logic (e.g., detecting inverter fault codes or BMS protection states and triggering mode overrides). They do not influence the current control logic. To use them, add them to the `solar_telemetry.qo` template and `note.add` body in `sendTelemetry()`, or wire them into `resolveMode()` for vendor-specific fault handling.
-
-**Transmitted.**
-- `solar_telemetry.qo` — one template-encoded Note every `report_minutes` (default 15 minutes, 96 records/day). Each record carries the most recently polled values from both devices plus the current operating mode string.
-- `dr_event.qo` — emitted immediately with `sync:true` on every mode transition. Includes the new and previous mode names, battery SOC, PV generation, and grid exchange at the moment of transition. This is the Note to route to a real-time notification channel.
-
-**Routed.** Notehub routes `solar_telemetry.qo` to a long-term analytics or historian system and `dr_event.qo` to an operational channel. Keeping the two Notefiles separate at the source means they can be fanned out differently with no filter logic in the route.
-
-**Inbound dispatch path.** An external energy management system, a utility DR bridge, or a simple scheduled script sends an HTTP POST to the Notehub API to queue a `dispatch.qi` Note on the target device. The Notecard delivers the command on its next inbound sync (≤5 minutes). The device acts on it in the following sample cycle (≤`sample_minutes` further). Total end-to-end latency from cloud dispatch to relay closure: **inbound cadence (5 minutes) + `sample_minutes`**. At the default `sample_minutes = 1` this is at most 6 minutes — inside the 10-minute DR response envelope most utilities require. The firmware clamps `sample_minutes` to 5, ensuring worst-case relay response stays at or within the 10-minute DR boundary regardless of the configured value.
-
-**What triggers mode changes.**
-- **TOU schedule** — `peak_discharge` engages automatically when UTC time enters the `peak_start_utc`→`peak_end_utc` window. When a non-zero `charge_start_utc`→`charge_end_utc` off-peak window is configured, `overnight_charge` also engages autonomously during that window. `normal` applies outside all configured windows. **Autonomous TOU transitions depend on valid Notecard UTC time.** On first boot or after a power event before the Notecard re-acquires time, `resolveMode()` receives `epoch = 0` and stays in `normal` — TOU windows are not evaluated until a valid epoch is available from at least one prior Notehub sync.
-- **Cloud dispatch** — any `mode` value in a `dispatch.qi` Note overrides the TOU schedule immediately. Dispatching `mode:"normal"` is not a no-op: it holds the controller in normal relay state and suppresses autonomous TOU transitions for the duration of any `expires_epoch` window (or until superseded). Use it to force a return to normal operation from the cloud during an active peak-discharge or DR window. On the wire the state is reported as `"normal"` in both `solar_telemetry.qo` and `dr_event.qo`.
-- **Dispatch expiry** — a `dispatch.qi` Note with an `expires_epoch` reverts to the TOU schedule (which may be `peak_discharge`, `overnight_charge`, or `normal` depending on the current UTC hour) when the epoch arrives. This provides a built-in recovery if the cloud system goes silent without sending an explicit release.
-- **SOC guard** — when SOC drops below `soc_min_pct`, or when the BMS becomes unreachable while `peak_discharge` is active, the device transitions to `low_soc_protect` mode: the discharge relay opens and a `dr_event.qo` mode-change event is emitted so operators can see why discharge was halted. The lower-guard latch engages at `soc_min_pct` and releases only after SOC recovers to `soc_min_pct + soc_hyst_pct` (default: 25 % with a 20 % minimum and 5 % band), preventing relay chatter when SOC readings oscillate near the threshold. BMS communication loss also latches the lower protect state; it clears only when comms are restored **and** SOC clears the hysteresis band on the same poll cycle. Additionally, BMS comm loss forces the upper-SOC **charge-inhibit** latch active in `applyRelays()` regardless of the current mode — charging is blocked until the BMS is reachable again and a live SOC reading confirms the battery has dropped below the `soc_max_pct − soc_max_hyst_pct` threshold. Both ends of the SOC operating range therefore fail safe symmetrically on BMS comm loss: discharge is blocked by the lower guard and charging is blocked by the upper guard.
-
-## 9. Validation and Testing
-
-
-
-**Pre-wiring check.** The shipped firmware has both TOU windows disabled by default (`peak_start_utc` and `peak_end_utc` both default to `0`; equal values = window off). The discharge relay will not assert autonomously until an operator explicitly sets a non-equal peak window via Notehub fleet environment variables. Verify the fleet environment variables are at their defaults (or intentionally set) before connecting relay outputs to live inverter or BMS control inputs. Enable and tune the peak window only after wiring, Modbus addressing, and SOC threshold validation are complete.
-
-
-
-**Cellular first-light.** Before connecting Modbus hardware or issuing dispatch commands, verify the Notecard has registered with Notehub. Issue a [`card.status`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-status) request from the [In-Browser Terminal](https://dev.blues.io/terminal/) and confirm `connected:true`. Follow with a [`hub.status`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-status) request to confirm the project association and that at least one sync has completed. Both commands can be issued without any tooling beyond a browser. Only proceed to Modbus and dispatch testing once cellular is confirmed.
-
-**Expected steady-state behavior.** In normal operation, the device generates one `solar_telemetry.qo` Note every 15 minutes and one `dr_event.qo` Note each time the mode transitions (typically twice per day, peak-window entry and exit). During commissioning, simulate a DR event by posting a `dispatch.qi` Note via the Notehub API with `"mode": "dr_curtail"` and `"expires_epoch"` set 10 minutes in the future. Within `5 min + sample_minutes` of queuing the Note (next inbound sync plus the following sample cycle), the `RELAY_DR_INDICATOR` relay should close and a `dr_event.qo` Note should appear in Notehub. At the default `sample_minutes = 1` this is at most **6 minutes**. If `sample_minutes` has been set to `5` (the firmware-enforced maximum), allow up to 10 minutes before expecting relay closure. Ten minutes after queuing the Note, the relay should release and a second `dr_event.qo` should record the reversion to the current scheduled mode (typically `normal` if the bench test is run outside any configured TOU window; run the test outside any active peak or charge window for a deterministic `normal` reversion).
-
-**Modbus first-light.** Before connecting real inverter and BMS hardware, validate the Modbus link using a USB-to-RS-485 adapter and a software Modbus simulator. Two widely used open-source simulators are:
-- **ModRSsim2** — [github.com/eModbus/ModRSsim2](https://github.com/eModbus/ModRSsim2) — cross-platform, provides an RS-485 virtual serial port
-- **pymodbus simulator** — `pip install pymodbus && python -m pymodbus.simulator tcp --host 127.0.0.1` (for TCP; can also use serial)
-
-Connect your USB-to-RS-485 adapter to the OPTA's RS-485 terminals and configure the OPTA's Modbus address and serial settings to match the simulator. Confirm that four-register reads from slave IDs 1 and 2 return the values the simulator is publishing. This approach is faster and safer than commissioning against live solar hardware.
-
-**Using Mojo to validate power behavior.** The Wireless for OPTA expansion runs from the 24 VDC supply. Splice the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the 24 VDC supply and the expansion's power input to capture session energy data. There are two distinct measurement layers to keep separate:
-
-**Layer 1 — Published Notecard supply-rail figures** (from the [NOTE-WBNAW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-wbnaw/) and the [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)). These describe the Notecard's own internal supply rail and are provided here for reference; they are **not** what Mojo will read at the 24 V expansion input:
-
-| Phase | Notecard supply-rail current |
-|---|---|
-| Idle (radio off, between syncs) | ~8–18 µA |
-| Cellular session (LTE Cat-1) | ~230–250 mA average; ≤2 A peak during transmit burst |
-| WiFi session (when WiFi fallback engaged) | ~80 mA average |
-
-**Layer 2 — What Mojo validates at the 24 V expansion input.** Because the Wireless for OPTA expansion uses a switching regulator to step the 24 V supply down to the Notecard's operating voltage, the current Mojo measures on the 24 V rail is substantially lower than the supply-rail figures above — the conversion ratio and regulator efficiency change both the magnitude and the shape of the current waveform. Validate the expansion subsystem in terms of *input power* (V × I as displayed by Mojo) and *session energy* (Mojo's coulomb counter integrated over each session) rather than expecting to read the supply-rail milliamp figures directly at the 24 V input.
-
-**Power acceptance criteria (target):** At 24 VDC input with default sync cadence (5-minute inbound, 15-minute outbound), the Wireless for OPTA expansion consumes approximately **1–2 mAh per 24-hour period** (dominated by inbound polling). Peak current during a cellular session is ≤200 mA at 24 V. These figures do not include the OPTA MCU itself, which adds a constant baseline draw of ~50–100 mA depending on firmware activity.
-
-The useful bench exercise is confirming that: (a) the 5-minute inbound cadence produces an energy pulse roughly every five minutes on the Mojo trace — with every third pulse at the 15-minute outbound boundary being visibly larger as it also flushes the outbound queue, and (b) a `sync:true` mode-change event produces an additional pulse outside the normal cadence. If instead you see continuously elevated power draw with no return to a lower idle level between pulses, the cellular session is not terminating correctly — check the `inbound` and `outbound` values in your `hub.set` configuration.
-
-Note that the OPTA itself is an always-on 24 VDC device whose steady-state draw will substantially exceed the Wireless for OPTA expansion's and will dominate the Mojo trace at the system level. For this bench measurement, read the Mojo trace in terms of the **periodic pulse pattern** rather than absolute current figures: look for the regular ~5-minute inbound pulses, the larger ~15-minute outbound pulses, and any out-of-cadence `sync:true` pulses, all riding on top of the OPTA's constant baseline. The Notecard low-power design guide's supply-rail figures (table above) are not directly visible on the 24 V rail; they are useful for understanding what fraction of each pulse is attributable to the Notecard versus the OPTA's own logic.
-
-## 10. Troubleshooting
-
-**No cellular connection or "Offline" in Notehub.**
-- Verify the external antenna is properly connected via SMA lead (rubber-duck antenna inside a metal cabinet will not work).
-- Check that the Wireless for OPTA is fully seated onto the OPTA's expansion port.
-- Ensure the 24 VDC power supply voltage is stable (use a multimeter at the OPTA power terminals).
-- Look for any error messages on the USB serial debug port (`115200 baud`). Issue a `card.status` from the [Blues In-Browser Terminal](https://dev.blues.io/terminal/) to check registration.
-
-**Modbus polling shows -9999 (device unreachable).**
-- Verify that inverter and BMS are configured to the same baud rate, parity, and stop bits. See `modbus_baud`, `modbus_parity`, `modbus_stop_bits` in Section 5.
-- Check that RS-485 A and B lines are wired correctly and terminators (120 Ω resistors) are present at both ends of the daisy-chain.
-- Confirm inverter and BMS slave IDs match the configured `modbus_slave_inv` and `modbus_slave_bms` environment variables (defaults: 1 and 2).
-- Use a USB-to-RS-485 adapter and a Modbus simulator to test the wiring in isolation before connecting to real hardware.
-
-**Relay doesn't respond to DR dispatch command.**
-- Verify dispatch latency: at `sample_minutes=1`, expect relay closure within 5–6 minutes. Longer if `sample_minutes` is higher.
-- Confirm the command was queued via the Notehub API by checking the device's `dispatch.qi` Notefile in the Notehub UI.
-- Verify the firmware is reading dispatch commands: call `checkDispatch()` in the loop and look for debug output on the serial port.
-- Ensure the relay output wires are connected to the correct field-device digital input terminals and that the input voltage rating matches.
-
-**Dispatch command expires too soon or TOU window doesn't transition as expected.**
-- Verify the Notecard has acquired valid UTC time from at least one Notehub sync. Call `card.time` from the [Blues In-Browser Terminal](https://dev.blues.io/terminal/) and confirm `time` is non-zero.
-- Check that TOU window hours (`peak_start_utc`, `peak_end_utc`, etc.) are in UTC. The firmware does not perform local-time conversion.
-- Verify the corresponding environment variables are set to non-equal values (equal values disable the window).
-
-## 11. Limitations and Next Steps
-
-This is a control-channel reference design, not a finished energy-management product. The simplifications below are deliberate scope choices — places where a real PPA operator or utility-integration partner will want to add vendor-specific register maps, deeper safety review, or a richer dispatch policy once the basic relay-and-uplink architecture is in place.
-
-### Simplified for this POC
-
-**Demo Modbus register map.** The firmware reads four contiguous 16-bit registers from each device with hardcoded scaling and signedness. Real commercial inverters (SolarEdge, Fronius, SMA, Huawei SUN2000, Sungrow) and BMS units (BYD Battery-Box, PYLON, CATL) each publish their own Modbus maps with vendor-specific register addresses, scaling factors, and word orders. Commissioning against real hardware requires a vendor-specific firmware build. See the Notehub `reg_inv_base` / `reg_bms_base` environment variables as a first step, but per-vendor register maps will need code changes for scaling and signedness.
-
-**Single shared Modbus RTU bus.** The firmware uses one RS-485 physical segment with one set of serial parameters (`modbus_baud`, `modbus_parity`, `modbus_stop_bits`) shared by both the inverter and the BMS. Both devices must be configurable to the same baud rate, parity, and stop bits — a hard commissioning prerequisite. On real sites this constraint is commonly violated when inverter and BMS come from different vendors with different factory defaults. If the two devices cannot be brought to the same serial settings, this reference topology is unworkable without hardware modifications (a second UART or USB-to-RS-485 adapter) and matching firmware changes to address each device on its own independent serial port.
-
-**SOC guard hysteresis defaults are proof-of-concept values.** The firmware applies anti-chatter hysteresis at both ends of the SOC operating range: `soc_hyst_pct` (default 5 %) prevents discharge-relay chatter near the `soc_min_pct` floor, and `soc_max_hyst_pct` (default 3 %) prevents charge-relay chatter near the `soc_max_pct` ceiling. Both are starting points, not field-tuned values. BMS units with high-noise SOC reporting or slow charge recovery may need wider bands; units with tightly regulated reporting may need narrower ones. Tune both variables for each site based on observed SOC reading jitter during commissioning. Note that the belt-and-suspenders hard guard in `applyRelays()` retains the unmodified `soc_min_pct` as an absolute discharge floor — it is not subject to hysteresis, so a brief over-protection window between `soc_min_pct` and `soc_min_pct + soc_hyst_pct` is intentional and prevents the discharge relay from toggling on a borderline reading.
-
-**Relay wiring is application-specific.** This reference design assumes the inverter and BMS each expose a digital control input for the functions mapped to the relay outputs. Not every commercial product does. Many inverters accept grid-export enable/disable via Modbus write command rather than a contact input; in that case the relay output is redundant and the appropriate command needs to be added to the firmware.
-
-**TOU schedule is UTC-only.** Peak window start and end are configured in UTC hours. Operators must convert their local TOU peak window to UTC. A `tz_offset_hours` environment variable and a proper local-time calculation would be a straightforward improvement.
-
-**Autonomous TOU dispatch requires valid Notecard UTC time.** `resolveMode()` calls `currentUtcEpoch()`, which issues a `card.time` request and returns `0` if the Notecard has not yet completed a Notehub sync. On cold boot or after a power event before the Notecard re-acquires time, TOU window evaluation is suspended and the controller stays in `normal` mode. TOU windows resume automatically once a non-zero epoch is available. Design the commissioning workflow so the Notecard has synced at least once — confirming that `card.time` returns a non-zero `time` field — before relying on autonomous TOU behavior in the field.
-
-**Relay contacts assert enable inputs only — actual dispatch behavior is vendor-defined.** All control is via relay contact closures that assert or de-assert digital enable inputs on the field devices. Closing a relay enables a mode on the field device; the actual charge, discharge, and export behavior — ramp rates, power limits, setpoints — is determined entirely by the inverter and BMS firmware and their pre-commissioning configuration. This proof of concept does not command power setpoints, verify that a requested profile was executed, or close any control loop around actual site load or grid flow. Commissioning against a specific inverter and BMS model requires verifying that asserting each control input produces the expected operational response.
-
-**No Modbus writes.** The firmware reads inverter and BMS state but all control is via relay contact closures. Writing Modbus setpoints to the devices (target SOC, charge current limit, export cap) is intentionally out of scope — it requires a safety review for each device type and is a meaningful production enhancement, not a sensible starting point for a proof-of-concept.
-
-**Single inverter and BMS per OPTA.** The firmware reads one inverter and one BMS. Sites with multiple strings or battery racks need either multiple OPTAs or firmware extended to round-robin across slave IDs on the same RS-485 bus.
-
-**One dispatch command consumed per sample cycle.** `checkDispatch()` pops exactly one Note from `dispatch.qi` per sample cycle. If commands arrive faster than `sample_minutes`, for example, a utility DR event and a rate-schedule override queued within the same 5-minute inbound sync window — they backlog in the Notecard queue and are processed one per cycle, up to `sample_minutes` apart per command. In the default 1-minute sample cadence this gap is small, but at longer cadences or during rapid command sequences the backlog can cause unexpected sequential mode transitions. Production systems with higher command rates should drain the queue within a single cycle (loop `checkDispatch()` until it returns no Note) or use idempotent commands with expiry timestamps.
-
-**Dispatch state held in RAM only.** `g_commanded_mode` and `g_dr_expires_epoch` are stored in RAM. A power cycle, watchdog reset, or MCU fault clears the active dispatch: the device reverts to the TOU schedule (or `normal` if outside all configured windows) until the cloud system resends the command. The Notehub telemetry stream will reflect the mode change, but there is no automatic re-delivery of the original command. For dispatch-critical deployments where a commanded relay state must survive power events, persist the active mode and expiry epoch to the Notecard's nonvolatile storage (a `note.add` to a `.db` Notefile on the Notecard) and restore them on boot before the first `resolveMode()` call — taking care to discard an expired epoch rather than re-applying a stale command.
-
-**Dispatch commands have no authentication.** Any Notehub API caller with a valid token can queue a `dispatch.qi` Note. Production deployments should scope API tokens to the minimum required permissions and consider note-level signing or encryption for additional assurance.
-
-**The TOU autonomous schedule alone is not a full energy management strategy.** The firmware dispatches peak discharge based on the configured hour window, but it doesn't account for forecast PV generation, real-time spot prices, battery degradation, or multi-day SOC planning. It's the right starting point, not the finished product.
-
-### Production Next Steps
-
-Taking this proof-of-concept toward a production deployment means hardening the device for the specific inverters, batteries, and reporting obligations of a real PPA portfolio. The following extensions are the natural progression, roughly from the most immediately useful to the most infrastructure-dependent.
-
-**Vendor-specific inverter profile builds** are the first thing most deployments will need. Rather than a generic SunSpec read, ship dedicated profiles for SolarEdge SunSpec, the Fronius Solar API, Huawei SUN2000, and Sungrow — each with the correct register addresses, scaling factors, and Modbus function codes for that manufacturer's map.
-
-**Modbus write support** is the largest functional leap. Inverters that accept export-cap or charge-limit commands over the bus could be commanded directly rather than through relay enable inputs alone, but this must be gated behind appropriate safety interlocks and a per-device safety review.
-
-**Local-time conversion** is a small but high-value usability improvement: a configurable `tz_offset_hours` environment variable so the TOU schedule can be expressed in local hours instead of UTC, which is how operators actually think about peak windows.
-
-**Multi-battery support** extends the controller to sites with more than one battery rack. Round-robin polling of up to N BMS units on the same RS-485 bus, with per-unit SOC tracking and aggregate discharge allocation, would let a single OPTA service a larger installation.
-
-**Fleet-level telemetry aggregation** turns the raw event stream into reporting. Rolling SOC and energy telemetry up through Notehub into a historian supports PPA performance reporting — total kWh discharged per DR event, aggregate daily export, battery cycle count, and similar settlement-grade metrics.
-
-**Over-the-air host firmware updates** are the longest-horizon item, because they currently depend on a hardware path that isn't yet available. [Notecard Outboard Firmware Update](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) is supported on the STM32H7 MCU family (the OPTA's processor) but requires AUX wiring that Blues Wireless for OPTA does not currently break out, so **host firmware updates remain local-only via USB-C until that hardware path changes.** When it does, Outboard DFU would enable pushing a new vendor register map or TOU algorithm to the entire fleet without a truck roll.
-
-## 12. Summary
-
-The PPA asset owner who used to depend on the building's IT department and a vendor's cloud portal now has their own line to the inverter and the battery: a DIN-rail PLC reading state over Modbus once a minute, four relays asserting the right mode at the right time, and a cellular uplink that doesn't care whose network sits on the other side of the cabinet door. When the utility calls a 4 PM DR event, the dispatch lands on the device within minutes, the export relay opens, and the telemetry trail goes straight to whoever needs it — energy manager, settlement system, or utility — without anyone agreeing on a dashboard vendor first.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/demand-response-solar-battery-dispatcher/)**
diff --git a/55-demand-response-solar-battery-dispatcher/banner.png b/55-demand-response-solar-battery-dispatcher/banner.png
deleted file mode 100644
index 43109960..00000000
Binary files a/55-demand-response-solar-battery-dispatcher/banner.png and /dev/null differ
diff --git a/55-demand-response-solar-battery-dispatcher/diagrams/01-system-architecture.svg b/55-demand-response-solar-battery-dispatcher/diagrams/01-system-architecture.svg
deleted file mode 100644
index 8fd8b8ed..00000000
--- a/55-demand-response-solar-battery-dispatcher/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,95 +0,0 @@
-
diff --git a/55-demand-response-solar-battery-dispatcher/diagrams/02-wiring-assembly.svg b/55-demand-response-solar-battery-dispatcher/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index a2ab2abe..00000000
--- a/55-demand-response-solar-battery-dispatcher/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,125 +0,0 @@
-
diff --git a/55-demand-response-solar-battery-dispatcher/diagrams/03-data-flow.svg b/55-demand-response-solar-battery-dispatcher/diagrams/03-data-flow.svg
deleted file mode 100644
index 1a3bbc5e..00000000
--- a/55-demand-response-solar-battery-dispatcher/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,81 +0,0 @@
-
diff --git a/56-commercial-plug-load-after-hours-waste-dashboard/README.md b/56-commercial-plug-load-after-hours-waste-dashboard/README.md
index 5b17900f..1a86a624 100644
--- a/56-commercial-plug-load-after-hours-waste-dashboard/README.md
+++ b/56-commercial-plug-load-after-hours-waste-dashboard/README.md
@@ -1,472 +1,5 @@
# Commercial Plug-Load & After-Hours Waste Dashboard
-
+A cellular monitor that clips CT clamps onto branch circuits in a commercial sub-panel and transmits hourly per-circuit power profiles so energy consultants can identify after-hours waste.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Commercial%20Plug-Load%20%26%20After-Hours%20Waste%20Dashboard).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/plug_load_monitor/](firmware/plug_load_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a cellular-connected [energy savings](https://blues.com/energy-savings/) monitor that clips non-invasive CT (current transformer) clamps onto branch circuits in a commercial sub-panel, samples RMS current once a minute, and transmits hourly per-circuit profiles to the [Blues Notehub](https://blues.com/notehub/) cloud service, giving energy consultants a view into which circuits are burning money at 2 AM without ever touching the building's corporate network.
-
-## 1. Project Overview
-
-
-**The problem.** In most commercial buildings, HVAC systems are heavily instrumented and often metered. Plug loads — the miscellaneous branch circuits feeding workstations, monitors, task lighting, small appliances, AV equipment, server closets, and vending machines — are usually invisible. A typical mid-size office building has 50 to 200 branch circuits on its sub-panels. A meaningful fraction of them are drawing load around the clock when they shouldn't be: workstations that employees never shut down, vending machines that never sleep, building signage that runs from midnight to 5 AM, and network closets cooling equipment that was decommissioned two years ago but never unplugged.
-
-An **ESCO** (Energy Service Company) or independent energy consultant is usually the right party to find and quantify this waste. They're hired specifically to audit the building and identify savings opportunities that the building owner can't see on their own. The problem is that ESCOs often can't deploy anything. Corporate IT departments view unmanaged IoT devices on the production WiFi as a security risk, and getting a new device approved and connected can take weeks or months. By the time the network form is processed, the consulting engagement has moved on, or the engagement's momentum has stalled. The ESCO ends up estimating plug loads from utility bills and walkthrough surveys instead of measuring them directly.
-
-**Why Notecard.** Cellular sidesteps the IT approval loop entirely. The Notecard Cell+WiFi connects to the public cellular network, not the building LAN, and needs no credentials, no VLAN, no firewall exception, and no IT ticket. Practically, this means the energy consultant shows up, clips CT clamps around four branch-circuit hot legs, and powers up the device — sampling starts on the very first 60-second wake. The first `circuit_summary.qo` Note (effectively a JSON payload) lands in Notehub on the next hourly outbound sync, so expect to see data within the first hour of installation. For faster commissioning visibility, set `report_interval_min=5` in Notehub before first power-up — env vars pre-provisioned in Notehub are delivered to the Notecard's local cache on the first successful cellular sync, and the firmware applies them on the next host wake after that sync. If the device is already powered and running, the firmware configures `inbound:360` (6-hour cadence), so a value changed in Notehub after boot will not reach the device for up to 6 hours. Notehub-terminal commands face the same constraint — they are queued for the Notecard's next scheduled inbound window and cannot wake a sleeping periodic device on demand. There's no network form to fill out, and no dependency on whoever manages the building's IT infrastructure. This is the deployment model cellular-first IoT was designed for: getting sensors into places where a normal network connection would take longer to provision than the entire project.
-
-
-
-The Notecard Cell+WiFi variant also retains WiFi as a fallback, so a site that does offer accessible WiFi can use it — without compromising the cellular-first deployment model for sites that don't.
-
-**Deployment scenario.** A small weatherproof enclosure mounted inside or adjacent to the target sub-panel. CT clamps clip around individual branch-circuit hot wires at the sub-panel — no wire cutting, no circuit interruption. The enclosure draws 5V DC from a compact AC/DC supply tapped at the panel feed. All panel work — including placing CT clamps inside the panel enclosure and landing the AC/DC supply on panel power — must be performed by a qualified person following site lockout/tagout procedures and applicable electrical codes. Once the enclosure is powered and in place, the CT clamps themselves clip on and off the circuit conductors without any further contact with live conductors, but the initial installation is not a DIY task. A typical engagement deploys one unit per sub-panel, leaving it in place for two to four weeks to capture enough time-of-day variation for the cloud classifier to build a reliable load profile per circuit.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Inside the enclosure next to the sub-panel, the Cygnet STM32L433 host on the Notecarrier CX wakes once a minute via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn), reads RMS current on each installed CT channel (A0–A3), and rolls the readings into per-circuit mean, peak, and active-minute accumulators. Once a reporting window closes, it hands a `circuit_summary.qo` Note to the Notecard, serializes its state into Notecard flash via `NotePayloadSaveAndSleep`, and cuts its own power rail until the next wake. The host is asleep more than 99% of the day.
-
-**Notecard responsibilities.** Whatever the host queues, the Notecard holds in its on-device [Note](https://dev.blues.io/api-reference/glossary/#note) queue until the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes) opens a cellular session and flushes everything in one go. WiFi is available as an opportunistic fallback for sites that happen to allow it. The same module pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) on every inbound sync — channel count, sampling cadence, idle thresholds, and (if enabled) business-hours configuration are all field-tunable without a firmware update.
-
-**Notehub responsibilities.** The embedded global SIM lands events in [Blues Notehub](https://notehub.io), which ingests them, stores every one, and applies the project's routes. The hourly `circuit_summary.qo` records flow downstream to whatever time-series database or analytics platform the ESCO's dashboard lives on. A single firmware image services every building in the engagement; [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) carry the per-site differences — time zone, business hours, CT model — without per-site firmware variants.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). The natural downstream for `circuit_summary.qo` is a time-series database or analytics platform where load-profile classification and dashboarding live (these are project-specific downstream integrations outside the scope of this reference design).
-
-## 3. Technical Summary
-
-After completing this README and deploying the firmware, you will have:
-
-- A Notecarrier CX with an integrated STM32 host running the plug-load monitor sketch, sampling four branch circuits at 60-second intervals.
-- One hourly `circuit_summary.qo` Note per device arriving in Notehub, carrying per-circuit mean RMS amps, peak RMS amps, and active-minutes (sample JSON below).
-- Optional: after-hours `circuit_alert.qo` Notes (real-time, `sync:true`) if you uncomment `PLUG_LOAD_ALERTS` in the firmware.
-- Environment variables editable in the Notehub Fleet UI (no firmware re-flash) to adjust thresholds and timing on running devices.
-
-**Example `circuit_summary.qo` from Notehub:**
-```json
-{
- "file": "circuit_summary.qo",
- "when": 1704067200,
- "body": {
- "ch1_mean": 8.4,
- "ch1_peak": 14.1,
- "ch1_act_min": 58.0,
- "ch2_mean": 0.1,
- "ch2_peak": 0.3,
- "ch2_act_min": 0.0,
- "ch3_mean": 12.7,
- "ch3_peak": 15.9,
- "ch3_act_min": 60.0,
- "ch4_mean": -9999.0,
- "ch4_peak": -9999.0,
- "ch4_act_min": -9999.0,
- "samples": 60
- }
-}
-```
-All values are RMS amps except `samples` (count) and `act_min` (minutes above idle threshold). Any field equal to `-9999.0` means that channel's CT was not installed.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "circuit_summary.qo",
- "when": 1704067200,
- "body": {
- "ch1_mean": 8.4,
- "ch1_peak": 14.1,
- "ch1_act_min": 58.0,
- "ch2_mean": 0.1,
- "ch2_peak": 0.3,
- "ch2_act_min": 0.0,
- "ch3_mean": 12.7,
- "ch3_peak": 15.9,
- "ch3_act_min": 60.0,
- "ch4_mean": -9999.0,
- "ch4_peak": -9999.0,
- "ch4_act_min": -9999.0,
- "samples": 60
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host — no separate MCU required. Exposes six analog inputs (A0–A5) on its dual 16-pin headers, four of which are used for CT channels. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular-first connectivity removes any dependency on site WiFi or LAN. The onboard WiFi radio is available as an opportunistic fallback. Ships with an active global SIM including 500 MB and 10 years of service. |
-| External cellular antenna, SMA, ~1 m lead (e.g. [SparkFun CEL-16432](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html)) | 1 | Required whenever the enclosure is mounted inside or adjacent to a metal sub-panel. A chip or flexible antenna left inside metalwork will not sustain reliable cellular connectivity. The SMA connector threads onto a bulkhead fitting threaded through a cable gland so the antenna element sits outside the metal. |
-| u.FL to SMA-female bulkhead pigtail, ~10–15 cm | 1 | Routes the Notecard's `MAIN` u.FL cellular port to the SMA bulkhead fitting on the enclosure wall. Keep as short as practical to minimize cable loss. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter for bench-level power validation; confirms the sleep/wake pattern is behaving as designed before field deployment. |
-| SCT-013-030 split-core CT, 30 A / 1 V (e.g. [SparkFun SEN-11005](https://www.sparkfun.com/products/11005)) | 4 | Non-invasive clamp-on current sensing — clips around the hot wire without breaking the circuit. The `-030` (1 V) variant has an internal burden resistor, so no external burden components are needed. 30 A rating covers standard 15 A and 20 A branch circuits with headroom; the `ct_full_scale_amps` env var re-scales if a different CT model is used. |
-| TRRS 3.5 mm breakout (e.g. [SparkFun BOB-11570](https://www.sparkfun.com/products/11570)) | 4 | The SCT-013-030 output cable terminates in a 3.5 mm TRRS plug. One breakout per CT brings the tip (signal) and sleeve (return) to screw-terminal connections. |
-| 10 kΩ 1% resistor (bias divider) | 2 | A two-resistor voltage divider between +3V3 and GND creates the shared bias mid-point (≈1.65 V) that all four CT channels reference. Deriving the bias from on-board 3.3 V keeps the signal centered in the ADC input range. |
-| 10 µF electrolytic capacitor | 1 | Decoupling on the shared bias node; suppresses divider noise that would otherwise add a low-frequency artifact to every RMS measurement. |
-| AC/DC supply, 5 V / 2 A (e.g. [MeanWell IRM-10-5](https://www.meanwell.com/Upload/PDF/IRM-10/IRM-10-SPEC.PDF)) | 1 | Derives 5 V DC from the 120 VAC panel feed for permanent, grid-tied power. A 2 A (10 W) rating is appropriate for a cellular design: the MBGLW's LTE Cat-1 bis modem (Quectel EG915 family) draws ~250 mA average during a session, and modem attach plus brief 2G/GSM-fallback transmit bursts in marginal-coverage areas can pull current well above that average from the 5 V rail during radio warm-up. An undersized supply that cannot source those peaks risks a brownout that aborts the cellular session. The IRM-10-5's 10 W output provides adequate headroom for the Notecard's peak draw, the host MCU active phase, and installation-environment margin. |
-| NEMA 4X enclosure, ~6×4×2″ | 1 | Encloses the electronics near the sub-panel; NEMA 4X rating handles the occasional humidity and spray found in mechanical rooms and utility corridors. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi seats in the carrier's M.2 slot. Mojo sits inline between the 5 V supply output and the Notecarrier's `+VBAT` pad.
-
-
-
-**Antenna placement.** The Notecard Cell+WiFi's cellular port is a u.FL connector (`MAIN`). For any installation inside or adjacent to a metal sub-panel or enclosure, connect a short u.FL-to-SMA-female bulkhead pigtail to the Notecard's `MAIN` u.FL port, thread the SMA bulkhead fitting through a cable gland in the enclosure wall, and screw the external cellular antenna onto the SMA fitting on the outside of the enclosure. A chip or flexible antenna left inside a metal enclosure will not maintain a reliable cellular link. The onboard WiFi chip antenna is similarly blocked by metalwork; if WiFi fallback matters at a given site, route a second u.FL pigtail from the Notecard's WiFi port to a second external antenna via the same cable-gland approach.
-
-**Safety.** Sub-panels contain hazardous voltages. CT installation must be performed by a qualified person following site lockout/tagout procedures and applicable electrical codes. The CT clamps themselves are non-invasive and do not require breaking or de-energizing the circuit being monitored. The AC/DC supply wiring *does* require connection to live conductors — always de-energize the panel before making line-voltage connections.
-
-
-
-**Shared bias circuit (build this once for all four channels):**
-
-1. Connect **+3V3** (Notecarrier CX header) → 10 kΩ R1 → **BIAS NODE**.
-2. Connect **BIAS NODE** → 10 kΩ R2 → **GND**.
-3. Connect the 10 µF capacitor from **BIAS NODE** to **GND** (electrolytic, positive lead to BIAS NODE).
-4. The BIAS NODE voltage will sit at approximately 1.65 V (half of 3.3 V). This is the common return reference for all four CT channels.
-
-**Per-channel CT connection (repeat for each CT):**
-
-- TRRS breakout **TIP** terminal → Notecarrier CX analog pin (**A0** for ch1, **A1** for ch2, **A2** for ch3, **A3** for ch4).
-- TRRS breakout **SLEEVE** terminal → **BIAS NODE** (the shared mid-point from above).
-- Plug the SCT-013-030's 3.5 mm TRRS cable into the breakout's jack.
-- Clip the CT clamp around **one** hot leg of the branch circuit to monitor. Clamping both legs of a single-phase 240 V circuit cancels the measurement — monitor one leg only per circuit.
-
-**Power and Mojo:**
-
-- 120 VAC feed from panel → MeanWell IRM-10-5 → **5 V DC output**.
-- Mojo `BAT` input ← 5 V DC output.
-- Mojo `LOAD` output → Notecarrier CX **+VBAT** pad.
-- Mojo Qwiic connector → Notecarrier CX Qwiic port (reports cumulative mAh to the Notecard for bench validation).
-
-Set the Notecarrier CX DIP switch to `HST` to route USB Serial to the onboard Cygnet host during firmware flashing and debug; flip to `NC` to route USB Serial to the Notecard for direct API testing.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Follow the [Notecard quickstart](https://dev.blues.io/quickstart/notecard-quickstart/notecard-and-notecarrier-pi/#set-up-notehub) if not already done.
-
-2. **Claim the Notecard and create a Fleet.** On first cellular session, the Notecard associates with your project automatically. Create a [Fleet](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) per building site (one fleet = one sub-panel deployment location). [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) allow per-site environment variables without maintaining per-device firmware variants.
-
-3. **Pre-provision environment variables (commissioning best practice).** Navigate to **Projects → [Your Project] → Fleets → [Your Fleet] → Environment** and add any non-default variables *before powering up the device for the first time*. This way they are delivered to the Notecard's local cache on the first successful cellular sync. All variables below are optional; firmware defaults are shown.
-
- **Sync timing:** The firmware configures `inbound:360`, meaning the Notecard checks Notehub for updated env vars every 6 hours. A variable changed in Notehub after the device is already running will not take effect until that next inbound sync. To force an immediate env-var pickup on an already-running device, connect a USB cable on-site, flip the Notecarrier CX DIP switch to `NC` (routes USB serial directly to the Notecard), and issue `{"req":"hub.sync"}` over the serial connection to trigger an immediate bidirectional session.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_sec` | `60` | Seconds between CT readings. Lowering this increases time resolution at the cost of host processor energy per hour; summary transmission volume is unchanged (one Note per `report_interval_min` regardless of sample rate). |
- | `report_interval_min` | `60` | Minutes between `circuit_summary.qo` Notes. Also updates the Notecard's `hub.set` outbound interval so the two stay in sync. |
- | `circuit_count` | `4` | Number of CT clamps installed (1–4). Channels above this count are not read and appear as `-9999` (no data) in summary Notes. If `circuit_count` is reduced while a summary window is already in progress, any samples already accumulated for the now-disabled channels are still emitted in the next summary; the change takes full effect at the following window boundary. |
- | `idle_threshold_amps` | `0.50` | RMS amps below which a circuit is treated as "off" for active-minute counting. Adjust upward if the leakage floor of a specific circuit generates false activity counts. |
- | `ct_full_scale_amps` | `30.0` | Full-scale primary current of the installed CT model. Override if using a different CT (e.g. `20.0` for a 20 A model). |
-
- The following variables are only read by the firmware when `PLUG_LOAD_ALERTS` is defined in `firmware/plug_load_monitor/plug_load_monitor_helpers.h` (see §7). They have no effect in the default build.
-
- | Variable | Default | Purpose (`PLUG_LOAD_ALERTS` builds only) |
- |---|---|---|
- | `after_hours_threshold_amps` | `2.0` | RMS amps above which an after-hours alert fires on a circuit that should be off. |
- | `biz_hours_start` | `8` | Local hour (0–23, inclusive) at which business hours begin. After-hours detection is inactive at or after this hour. |
- | `biz_hours_end` | `18` | Local hour (0–23, exclusive) at which business hours end. |
- | `tz_offset_hours` | `0` | **Whole-integer** hours offset from UTC to convert Notecard timestamps to local time for business-hours evaluation. For US Eastern Standard Time use `-5`; Eastern Daylight Time use `-4`. The firmware stores this value as `int8_t` and does integer arithmetic; sites in half-hour (e.g. India IST UTC+5:30) or 45-minute (e.g. Nepal UTC+5:45) offset zones are not supported — business-hours windows will be evaluated incorrectly at those sites. |
- | `alert_cooldown_min` | `60` | Minimum minutes between repeat after-hours alerts on the same circuit. Prevents alert fatigue on a circuit that holds a sustained load throughout the night. |
-
-4. **Configure data routes (Projects → Data & Routing).** Add a [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `circuit_summary.qo` (batched hourly records, forward to a time-series database or analytics platform for load-profile classification). If building with `PLUG_LOAD_ALERTS` enabled, add a second route for `circuit_alert.qo` — low-volume, time-sensitive records forwarded to an email, Slack, or CMMS destination.
-
-## 7. Firmware Design
-
-Three files in `firmware/`:
-
-| File | Role |
-|---|---|
-| [`plug_load_monitor.ino`](firmware/plug_load_monitor/plug_load_monitor.ino) | Arduino sketch — `setup()` and `loop()`, global variable definitions, sleep/wake orchestration |
-| [`plug_load_monitor_helpers.h`](firmware/plug_load_monitor/plug_load_monitor_helpers.h) | Shared constants, types, `extern` declarations, function prototypes, and feature flags (`PLUG_LOAD_DEBUG`, `PLUG_LOAD_ALERTS`) |
-| [`plug_load_monitor_helpers.cpp`](firmware/plug_load_monitor/plug_load_monitor_helpers.cpp) | All helper implementations — hub config, template registration, env-var fetch, CT measurement, Note emission |
-
-**Dependencies:**
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (`note-arduino` library). Install via the Arduino Library Manager (`arduino-cli lib install "Blues Wireless Notecard"`). Verify the latest version at the [note-arduino releases page](https://github.com/blues/note-arduino/releases).
-
-**Optional alert extension.** The `PLUG_LOAD_ALERTS` flag in `plug_load_monitor_helpers.h` is commented out by default. Uncommenting it adds the `circuit_alert.qo` Notefile: a `note.template` registration, a `card.time` call each wake, business-hours evaluation, and an immediate `sync:true` alert Note when a circuit exceeds `after_hours_threshold_amps` outside business hours. The baseline build (flag undefined) compiles none of this code — no extra I²C calls, no second Notefile, no unscheduled cellular sessions.
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard hub configuration (`hub.set`, outbound cadence) | `hubConfigure` |
-| Note template registration (`circuit_summary.qo`) | `defineTemplates` |
-| Accelerometer quiesce on first boot (`card.motion.mode`) | `setup()` |
-| Env-variable fetch and local config update | `fetchEnvOverrides` |
-| Per-channel RMS current measurement | `readChannelAmpsRMS` |
-| Per-cycle accumulation and summary trigger | `runSampleCycle` |
-| Summary Note emission and window reset | `sendSummary` |
-| Sleep/wake state serialization | `setup` / `loop` via `NotePayloadSaveAndSleep` |
-| After-hours time-of-day check _(requires `PLUG_LOAD_ALERTS`)_ | `isAfterHours` |
-| Immediate after-hours alert emission _(requires `PLUG_LOAD_ALERTS`)_ | `sendAlert` |
-
-### Sensor reading strategy
-
-Each CT channel uses a two-pass ADC approach lifted directly from the Open Energy Monitor technique. In the first pass, `CT_BIAS_SAMPLES` (256) readings are averaged to measure the actual DC offset of the bias network — the shared voltage divider is nominally at 1.65 V, but real-world resistor tolerances mean deriving the offset empirically is more accurate than assuming it. In the second pass, `CT_RMS_SAMPLES` (1480) readings have the DC offset subtracted, are squared and summed, then divided and square-rooted to produce an RMS count value. That count converts to volts at 3.3 V / 4095 counts, then scales to amps at 30 A per 1 V RMS (the SCT-013-030 rated output). The 1480-sample window is an empirically chosen RMS measurement window; actual elapsed time and mains-cycle coverage depend on the platform's ADC conversion timing, which varies across STM32 Arduino core configurations and clock speeds. The window is sufficient for a stable single-sample RMS estimate in practice, but exact cycle coverage should be bench-validated on the target hardware build rather than assumed from the sample count alone.
-
-The 12-bit ADC on the STM32L433 is enabled explicitly with `analogReadResolution(12)` in `setup()` — the Arduino STM32 core defaults to 10-bit if this call is omitted, which would reduce current-sensing resolution by 4×.
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates store records as fixed-length binary rather than free-form JSON, reducing on-wire size roughly 3–5× — material for a device transmitting 24 summary Notes per day per panel over a multi-month engagement.
-
-`circuit_summary.qo` (hourly):
-
-```json
-{
- "file": "circuit_summary.qo",
- "body": {
- "ch1_mean": 8.4,
- "ch1_peak": 14.1,
- "ch1_act_min": 58.0,
- "ch2_mean": 0.1,
- "ch2_peak": 0.3,
- "ch2_act_min": 0.0,
- "ch3_mean": 12.7,
- "ch3_peak": 15.9,
- "ch3_act_min": 60.0,
- "ch4_mean": -9999.0,
- "ch4_peak": -9999.0,
- "ch4_act_min": -9999.0,
- "samples": 60
- }
-}
-```
-
-Here all three ch4 fields carry `-9999.0` (the `INVALID_SENTINEL`) because `circuit_count` is set to `3` for this deployment — only three CT clamps are installed. When `n_arms[ch] == 0` (no valid samples taken for that channel during the window), the firmware emits `INVALID_SENTINEL` for `chN_mean`, `chN_peak`, **and** `chN_act_min`. Downstream consumers must treat any field equal to `-9999.0` as "no data" rather than as a zero-amp or zero-minute reading.
-
-`act_min` is derived from one-minute snapshots, not from continuous waveform occupancy measurement: each sample whose RMS current meets or exceeds `idle_threshold_amps` contributes `sample_interval_sec` seconds (default 60 seconds) to the active total, which is then divided by 60 at emit time. A circuit that is active for only part of a sample interval scores identically to one active for the whole interval.
-
-
-
-**`circuit_alert.qo` (immediate, `sync:true`) — requires `PLUG_LOAD_ALERTS`:** When the alert extension is enabled by defining `PLUG_LOAD_ALERTS` in `plug_load_monitor_helpers.h`, the firmware also emits a second Notefile for real-time notification. The template registration, `sendAlert()` function, `notecardEpoch()`, `isAfterHours()`, and the alert-gating block in `runSampleCycle()` are all gated on `#ifdef PLUG_LOAD_ALERTS` in `plug_load_monitor_helpers.cpp`. The core summary stream is identical whether or not the alert extension is compiled in.
-
-```json
-{
- "file": "circuit_alert.qo",
- "body": {
- "circuit": 3,
- "arms": 12.7,
- "alert_type": "after_hours_load",
- "hour_local": 2
- }
-}
-```
-
-The `hour_local` field carries the firmware's local-time computation (UTC plus `tz_offset_hours`) so the alert is immediately human-readable in Notehub without knowing the site's time zone configuration.
-
-
-
-### Low-power strategy
-
-Even though the enclosure is grid-tied to 120 VAC, the host runs in deep sleep between samples. After each sample cycle the host calls `NotePayloadSaveAndSleep`, which serializes the `AppState` struct into Notecard flash and issues [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to cut the host power rail for `sample_interval_sec` seconds. The Notecard itself enters its [low-power idle state](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) (~8–18 µA @ 5 V) between cellular sessions. Sampling and transmitting are deliberately decoupled: the host wakes every 60 seconds but the Notecard only opens a cellular session once an hour. When the `PLUG_LOAD_ALERTS` extension is enabled, alert Notes carry `sync:true`, bypassing the hourly window so a 2 AM anomaly pages immediately rather than waiting until the next scheduled session, but this is not active in the default build.
-
-When ATTN is not gating host power — typical of bench setups over USB without the full Notecarrier CX power path active — `NotePayloadSaveAndSleep` returns without cutting the rail and `loop()` drives the sample cadence itself: it waits `sample_interval_sec` seconds, re-reads env vars, and calls `runSampleCycle()` before the next sleep attempt. The accumulation, alert, and summary logic are identical on both paths.
-
-Running the host asleep between samples also keeps the design portable: the same firmware runs identically from a small backup battery or a solar cell if the site ever requires that.
-
-### Retry and error handling
-
-The firmware's resilience comes from a handful of deliberate choices, each guarding against a specific failure mode that shows up in real field deployments. They are worth understanding together, because they interlock.
-
-**Surviving the cold-boot I²C race.** The first `hub.set` call uses `sendRequestWithRetry(req, 10)` with a 10-second window. This covers the cold-boot I²C race condition documented in the `note-arduino` library, where the host comes up before the Notecard's I²C peripheral is ready. Without the retry window, the very first request after power-up could fail simply because the host won the boot race.
-
-**Distinguishing a zero-load circuit from a sensor fault — and why this design does not.** `readChannelAmpsRMS` clamps its return value to zero if the computed value is negative (a defensive guard; the sqrt-based computation will not naturally produce a negative result under normal ADC conditions). It is important to understand what a near-zero reading does and does not tell you. **A CT that is plugged in but clamped around a conductor carrying near-zero current reads near zero** — the CT output shorts tip to sleeve at the bias potential, so the bias-subtracted RMS is genuinely small. **By contrast, a CT cable that is *unplugged* leaves the analog input floating:** with no drive path to the bias node, the pin can pick up arbitrary levels through stray 60 Hz capacitive coupling from the panel environment and produce large or erratic ADC readings, *not* a predictable near-zero floor. Crucially, **there is no explicit per-channel fault-detection mechanism in this firmware**; open-input and zero-load conditions are not distinguished. Treat that as a known limitation rather than a guarantee that every channel is healthy.
-
-**The `INVALID_SENTINEL` value is not a sensor-fault flag.** The `INVALID_SENTINEL` (`-9999.0`) value in summary Notes is emitted only when `n_arms[ch] == 0` at summary time. In practice, every configured channel receives a read on every wake cycle, so this guard is rarely reached; its primary purpose is to protect the `safeAvg` helper against a logic fault where a channel is skipped entirely. **Downstream consumers should not treat it as a per-sample sensor-fault indicator.**
-
-**Retaining last-known-good configuration through transient I²C failures.** `fetchEnvOverrides` makes up to two attempts — an initial request and one retry after a 250-millisecond pause — to absorb transient Notecard I²C hiccups at wake. On success, the effective configuration is captured into `AppState.saved_cfg` and persisted alongside the accumulator state in the `NotePayloadSaveAndSleep` payload. On subsequent wakes the saved configuration is restored from `saved_cfg` *before* `fetchEnvOverrides` is called, so **a transient I²C failure retains the last known-good values rather than silently reverting to compile-time defaults for that cycle.** On cold boot, `env.get` returns an empty body because the Notecard's local cache is not yet populated — the device runs on compile-time defaults for that first wake.
-
-There is an important timing consequence here. Env vars pre-provisioned in Notehub before power-up reach the Notecard's local cache during the first successful cellular sync; `fetchEnvOverrides` picks them up on the next host wake after that sync, and if `report_interval_min` changed, `hubConfigure` re-applies the new outbound cadence immediately. But note that **`hubConfigure` sets `inbound:360`, meaning the Notecard pulls fresh env vars from Notehub only every 6 hours.** An env var changed in Notehub while the device is already running will *not* appear on the device until that next inbound sync fires — it does not take effect on the next 60-second host wake. To push a change sooner, connect a USB cable on-site, flip the Notecarrier CX DIP switch to `NC`, and issue `{"req":"hub.sync"}` directly over the serial connection. Be aware that **Notehub-terminal commands are subject to the same `inbound:360` delivery window and cannot trigger an immediate sync on a sleeping periodic device.**
-
-**Idempotent template definition survives card resets and swaps.** `defineTemplates` is called unconditionally at every boot — `note.template` is idempotent, so re-issuing it on an intact Notecard is a no-op. Re-issuing after a Notecard factory reset or card replacement restores the fixed-schema binary encoding *before* any `note.add` calls reach the Notecard, eliminating the window where Notes could be queued against a missing template. Template-confirmation flags are tracked **per Notefile** (`g_summary_template_applied`, and `g_alert_template_applied` in `PLUG_LOAD_ALERTS` builds), so a transient I²C failure registering one template does not gate emission on the other Notefile. Both flags are non-persisted per-boot variables; they are deliberately *not* stored in `AppState`, because a host-side boolean cannot reliably reflect Notecard state across a card reset or swap.
-
-**Alert cooldowns persist across sleep.** Per-channel alert cooldowns (`alert_last_unix[]`) are persisted across sleep cycles in the `AppState` payload, so **a circuit that fires an alert at 23:55 cannot fire again until `alert_cooldown_min` has elapsed** — regardless of how many sleep boundaries fall in between.
-
-### Key code snippet 1: template definition
-
-The template tells the Notecard the fixed schema for summary records. `14.1` encodes a 4-byte IEEE 754 float; `12` encodes a 2-byte signed integer. Every field name here must exactly match the `note.add` body fields in `sendSummary`.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "circuit_summary.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "ch1_mean", 14.1);
-JAddNumberToObject(body, "ch1_peak", 14.1);
-JAddNumberToObject(body, "ch1_act_min", 14.1);
-// ... repeated for ch2–ch4 ...
-JAddNumberToObject(body, "samples", 12);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 2: immediate-sync alert (PLUG_LOAD_ALERTS builds only)
-
-`sync:true` tells the Notecard to open a session immediately rather than waiting for the next scheduled outbound window. When `PLUG_LOAD_ALERTS` is defined, an after-hours alert Note arrives in Notehub within the session-establishment latency (~15–60 seconds) of the threshold trip. The full implementation lives in `plug_load_monitor_helpers.cpp` inside `#ifdef PLUG_LOAD_ALERTS` guards (`sendAlert()` and the alert-gating block inside `runSampleCycle()`).
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "circuit_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "circuit", (int)circuit_1based);
-JAddNumberToObject(body, "arms", arms);
-JAddStringToObject(body, "alert_type", "after_hours_load");
-JAddNumberToObject(body, "hour_local", hour_local);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 3: two-pass RMS measurement
-
-```cpp
-// Pass 1: measure DC bias offset
-uint32_t acc = 0;
-for (uint16_t i = 0; i < CT_BIAS_SAMPLES; i++) acc += analogRead(pin);
-int32_t dc_offset = (int32_t)(acc / CT_BIAS_SAMPLES);
-
-// Pass 2: compute RMS of the AC component
-uint64_t sum_sq = 0;
-for (uint16_t i = 0; i < CT_RMS_SAMPLES; i++) {
- int32_t s = (int32_t)analogRead(pin) - dc_offset;
- sum_sq += (uint64_t)((int64_t)s * s);
-}
-float rms_counts = sqrtf((float)sum_sq / (float)CT_RMS_SAMPLES);
-float rms_v = rms_counts * ADC_VREF_V / (float)ADC_COUNTS;
-float arms = rms_v * (CFG_CT_FULL_SCALE_AMPS / CT_VOUT_AT_FULL_SCALE);
-```
-
-### Key code snippet 4: sleep between samples
-
-```cpp
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, CFG_SAMPLE_INTERVAL_SEC, NULL);
-```
-
-`NotePayloadSaveAndSleep` writes the `AppState` struct into Notecard's non-volatile storage and then issues `card.attn` to cut the host power rail. On the next wake, `NotePayloadRetrieveAfterSleep` + `NotePayloadGetSegment` rehydrates the state before any code in `setup()` runs.
-
-## 8. Build and Flash
-
-1. **Install the Arduino STM32 core and Notecard library:**
- ```bash
- arduino-cli core install STMicroelectronics:stm32
- arduino-cli lib install "Blues Wireless Notecard"
- ```
-
-2. **Set your Notehub ProductUID.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/plug_load_monitor/plug_load_monitor_helpers.h` as `PRODUCT_UID`. (The define lives in the shared header so it is visible to both `.ino` and `.cpp` translation units.)
-
-3. **Compile and flash.** Connect the Notecarrier CX via USB and set the DIP switch to `HST` (routes USB serial to the host MCU). Flash with:
- ```bash
- arduino-cli compile -b STMicroelectronics:stm32:Nucleo_L433RC_P \
- --build-properties build.extra_flags="-DNOTECARD_USE_SERIAL" \
- firmware/plug_load_monitor/plug_load_monitor.ino
- ```
- Follow the [stm32duino](https://github.com/stm32duino/Arduino_Core_STM32) upload instructions for your operating system (may require manual bootloader entry; consult the Notecarrier CX datasheet pin-out). Alternatively, use the Arduino IDE: open the sketch, select board `STMicroelectronics → STM32L4 Series → Nucleo L433RC (P)`, compile, and upload.
-
-## 9. Data Flow
-
-
-
-Every 60 seconds the host wakes, reads all active CT channels, and checks whether the hourly summary window has elapsed.
-
-- **Collected (per wake, per channel).** Single-sample RMS amps. The sample is immediately accumulated into the rolling window; it is not individually transmitted.
-- **Transmitted.**
- - `circuit_summary.qo` — one record every `report_interval_min` (default 60 minutes, 24 records/day/device). Each record carries mean RMS amps, peak RMS amps, and active-minutes for each of the four channels, plus total sample count. Active-minutes (`act_min`) is a thresholded sample-count estimate — each one-minute snapshot above `idle_threshold_amps` contributes one minute; it is not derived from continuous waveform analysis (see §6 for the precise computation). The Notecard's automatic UTC timestamp enables time-of-day analysis in the downstream system.
- - `circuit_alert.qo` — only present when `PLUG_LOAD_ALERTS` is defined. Emitted when `arms >= after_hours_threshold_amps` during non-business hours, with `sync:true` for immediate delivery. Rate-limited per channel to once per `alert_cooldown_min`. Not part of the default build.
-- **Routed.** `circuit_summary.qo` records land in Notehub and route to a time-series store for load-profile classification. When `PLUG_LOAD_ALERTS` is enabled, `circuit_alert.qo` records route separately to a real-time channel.
-- **Downstream classification.** A downstream classifier uses the rolling `circuit_summary.qo` stream to assign each circuit a sustained load profile — always-on, scheduled, or occupied-hours — based on how `act_min` and `mean` vary across the time-of-day distribution over the deployment period. Classification and dashboarding are project-specific integrations outside the scope of this reference design.
-
-## 10. Validation and Testing
-
-**Expected steady-state cadence.** In a correctly-behaving deployment, one `circuit_summary.qo` Note arrives in Notehub per hour per device. In a `PLUG_LOAD_ALERTS` build, the `circuit_alert.qo` file should be quiet during business hours; after-hours activity depends entirely on what the building's circuits are actually doing — an always-on server room generates a steady non-zero mean across all hours, while a circuit powering desktop workstations should trend toward zero after business hours. In the default build, `circuit_alert.qo` is never created.
-
-**Bench first-light.** Before field deployment, verify the RMS readings make sense on the bench. Clamp one CT around a known load (a lamp, a fan, or a lab power supply with a known current draw), confirm the `ch1_mean` field in the first summary Note is in the expected range, and verify that `ch1_act_min` equals the number of minutes the load was on during the window. To confirm the near-zero floor, plug a CT into its TRRS breakout and clip it around a conductor carrying no current (or a de-energized wire): with the CT plugged in but unloaded, the CT's internal burden resistor holds the tip close to the sleeve potential, so the bias-subtracted RMS should be stable and well below `idle_threshold_amps`. This is distinct from leaving the TRRS jack *unplugged* — with no CT inserted, the analog input floats and can produce large, erratic readings driven by stray 60 Hz capacitive pickup from the panel environment; do not use a floating-input reading as a "zero amps" baseline.
-
-**Using Mojo to validate power behavior.** Mojo, wired inline at the Notecarrier CX `+VBAT` pad, measures the **whole powered subsystem** — Notecard plus Notecarrier CX on-board regulators, not the bare Notecard alone. The Notecard's published datasheet figures are shown for reference; expect the actual whole-assembly idle floor measured by Mojo to be somewhat higher than the bare Notecard figure, because the Notecarrier CX on-board LDOs draw quiescent current even when the host MCU power gate is open. During an active cellular session, the modem dominates and the whole-assembly reading tracks the datasheet value closely. Use Mojo to validate the *pattern* of power consumption rather than matching a single threshold against a Notecard-only spec.
-
-| Phase | Notecard datasheet (bare Notecard only) |
-|---|---|
-| Deep sleep (radio off, host gated off) | ~8–18 µA @ 5 V. See [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) |
-| Host awake, sampling 4 CT channels | — (host MCU run-mode + 4× ADC bursts, ~100–300 milliseconds; consult the STM32L433 datasheet and confirm the actual whole-assembly figure with Mojo on your bench) |
-| Cellular session (LTE Cat-1 bis, single Note) | ~250 mA average; brief higher-current bursts during modem attach and any 2G/GSM-fallback transmit |
-
-**After-hours alert testing (`PLUG_LOAD_ALERTS` builds only).** When `PLUG_LOAD_ALERTS` is defined and the firmware is rebuilt, `isAfterHours()` returns `false` whenever `card.time` reports epoch 0 — business-hours evaluation is inactive until the Notecard has acquired valid network time from a successful cellular (or WiFi) session. On first light or in a no-service bench environment, no after-hours alert will fire even if a circuit exceeds `after_hours_threshold_amps`. Confirm the device has appeared in Notehub (proving at least one successful session) before expecting after-hours alert behavior in testing. In the default build this function is not compiled and no alert testing is needed.
-
-Splice the [Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) inline between the AC/DC supply output and the Notecarrier CX `+VBAT` pad. A healthy power trace should show: brief active pulses every 60 seconds (host awake and sampling, typically 1–3 seconds each) followed by a quiet low-current floor, then a longer, higher-current session once per hour when the Notecard transmits. If the trace shows no quiet floor between active phases — the current stays continuously elevated with no periodic drop — the host is likely not sleeping; the most common cause is `card.attn` not reaching the host EN pin correctly. If the current shows continuous high draw at all times, `hub.set` may still be in `continuous` mode from a prior bench session; with the Notecarrier CX DIP switch in `NC` mode, issue `{"req":"hub.set","mode":"periodic","outbound":60,"inbound":360}` directly over the USB serial connection to correct it.
-
-## 11. Troubleshooting
-
-**No Notehub activity after first power-up (no claims, no Notes in Notehub, no LED activity on Notecard).**
-- Confirm the external cellular antenna is screwed onto the SMA bulkhead on the enclosure exterior (not inside the metal panel). A chip or flexible antenna inside metalwork will not obtain a cellular lock.
-- Check that the u.FL to SMA pigtail is securely seated on the Notecard's `MAIN` u.FL connector.
-- Verify the Notecarrier CX DIP switch is not set to `HST` (which routes the host MCU's USB UART to debug); set it to a power-off state if not actively flashing. The default position routes 5V supply.
-- If using WiFi fallback, verify the onboard WiFi antenna is unobstructed; the same metal-enclosure blocking applies.
-
-**Non-zero RMS readings on a circuit with no load (CT plugged in, clamp on de-energized or zero-current wire).**
-- This is normal. The CT's internal burden resistor holds the tip close to the bias node (≈1.65 V), so the RMS is small and stable, typically well below the idle threshold. This is distinct from an unplugged CT (next item).
-
-**Erratic or large readings on one or more channels; no pattern to the values across samples.**
-- Confirm the TRRS jack is **fully seated** in the breakout. A partially seated jack leaves the tip floating, allowing stray 60 Hz capacitive coupling from the panel environment to drive large, noisy ADC readings.
-- Confirm the SLEEVE terminal is connected to the BIAS NODE. If the sleeve is floating or connected elsewhere, the AC signal has no return path and the ADC input can drift and pick up arbitrary noise.
-
-**First summary Note appears after many hours, not within the first hour.**
-- The device defaults to a 60-minute reporting window. If powered mid-window, the first Note arrives on the next hourly boundary (up to 60 minutes after power-up). To validate faster, set `report_interval_min=5` in Notehub *before* first power-up, then look for a summary Note within 5 minutes of the device appearing in Notehub.
-
-**Summary Notes appear with expected values, but alerts (if using `PLUG_LOAD_ALERTS`) never fire.**
-- Confirm `PLUG_LOAD_ALERTS` is uncommented in `firmware/plug_load_monitor/plug_load_monitor_helpers.h` before building.
-- Verify the device has successfully claimed in Notehub (proves at least one cellular session). Business-hours evaluation requires valid network time from `card.time`, which only returns a meaningful epoch after the first successful cellular (or WiFi) sync.
-- Confirm `biz_hours_start`, `biz_hours_end`, and `tz_offset_hours` are set correctly in the Fleet environment variables. By default, business hours are 8 AM–6 PM UTC (which is only meaningful if the building is in the UTC timezone).
-
-## 12. Limitations and Next Steps
-
-This design is built around a single deployment idea: a consultant clipping CT clamps onto branch circuits and leaving the device for a few weeks. The simplifications below are scope choices that match that engagement model — places where a longer-term install, a billing-grade meter, or a richer alerting layer would want to extend it.
-
-### Simplified for this Proof-of-Concept
-
-The following are scope choices, not oversights. Each one trades a capability that a longer-term install would want against the simplicity that makes a few-week consulting engagement practical to deploy.
-
-**Single-phase hot leg only.** Each CT clamp monitors one hot leg. A 240 V split-phase circuit — a large appliance or an HVAC unit, for example — requires two CTs and a current sum in firmware, and **three-phase circuits need three CTs per circuit and are not supported in this design.**
-
-**No actual wattage.** Without a voltage measurement, the firmware reports **amps, not watts.** True power (W) = V × A × power factor, so any wattage or kWh figure derived from amps alone requires site-specific voltage and power-factor assumptions. Without direct measurement these estimates can be substantially wrong for mixed commercial plug loads, where power factor varies widely by device type — so **treat amp-only energy calculations as rough order-of-magnitude indicators, not billable measurements.** Adding true-power measurement requires an isolated voltage reference (via a low-ratio isolation transformer feeding the MCU ADC, or a dedicated energy-metering analog front end (AFE) with built-in isolation), along with phase-aligned sampling of voltage and current waveforms and power-factor computation. **A resistive voltage divider from mains to the MCU ADC is not a safe path:** it lacks the isolation required to protect the circuit and the installer from line voltage. One isolated voltage reference per phase is typically sufficient for all circuits on that phase, but the voltage and current samples must be time-synchronized for the power-factor computation to be accurate.
-
-**Up to four circuits.** The Notecarrier CX has six analog inputs (A0–A5); this design uses four (A0–A3). `MAX_CHANNELS` is hard-coded to `4`, `circuit_count` is clamped to 1–4 in firmware, and the `note.template` body and `sendSummary` field list define fields only for `ch1`–`ch4`. The important caveat here is that **extending to six circuits is a firmware change, not just a hardware change:** the pin map, `MAX_CHANNELS` constant, state arrays (`sum_arms`, `peak_arms`, `n_arms`, `active_samples`, `alert_last_unix`), `note.template` body fields, `sendSummary` field list, and any downstream schema or dashboard that assumes four channels would all require updating. Two more CT + breakout pairs complete the hardware side.
-
-**Classifier is a downstream integration.** Building a deployable cloud function (AWS Lambda, Google Cloud Functions, or similar), connecting it to a time-series database, and wiring it to a dashboard is a project-specific integration step **outside the scope of this reference design.** The firmware correctly produces the `circuit_summary.qo` inputs that a classifier needs. One consequence is worth flagging when `PLUG_LOAD_ALERTS` is enabled: until a classifier is deployed and an allowlist of legitimately always-on circuits is established, `circuit_alert.qo` will fire for *any* circuit exceeding `after_hours_threshold_amps` at night — including server rooms, 24/7 signage, and other intentional always-on loads. **Expect false-positive alerts during the initial deployment period.**
-
-**Business-hours detection uses simple hour-of-day (`PLUG_LOAD_ALERTS` only).** When the alert extension is enabled, after-hours detection is based on hour of day only; **it does not account for weekends, holidays, or shift patterns.** A more sophisticated implementation would pull a site calendar from an environment variable or a Notehub inbound Note. Note also that **`tz_offset_hours` only supports whole-hour UTC offsets** (stored as `int8_t`); sites in half-hour or 45-minute UTC-offset zones will have their business-hours windows evaluated incorrectly.
-
-**Mojo is bench equipment.** The firmware does not read the Mojo's LTC2959 coulomb counter over the Qwiic bus during normal operation. Adding a `mojo_mah` field to the summary Note is a straightforward extension for deployments where fleet-level energy budgeting is valuable.
-
-**No explicit open-CT or stuck-channel detection.** The firmware does not distinguish between two distinct conditions: **(a)** a CT that is plugged in and clamped around a conductor carrying near-zero current, which legitimately reads near zero; and **(b)** a CT cable that is unplugged or an input channel that is otherwise open-circuit. An unplugged CT leaves the analog input floating — the sleeve's bias path is only present when the CT cable is seated in the TRRS jack — which means the floating pin can produce arbitrary, noisy ADC values driven by stray 60 Hz capacitive pickup, *not* a reliable near-zero floor. Without a defined pull from the analog input to the bias node (for example, a weak bleed resistor across the TRRS tip-to-sleeve terminals on the breakout), open inputs are indistinguishable in firmware from genuine low-current readings. **Production firmware should add open-channel detection** — a hardware pull to bias on the analog input side, or software checks for implausibly constant or erratic readings across many cycles — so operators can distinguish a sensor fault from a genuinely unloaded circuit.
-
-**Sensor accuracy.** The **SCT-013-030 is rated ±3% at rated current** per the datasheet, with reduced accuracy at low currents (below ~5% of full scale, or 1.5 A for a 30 A clamp). **Sub-1 A readings are indicative only.**
-
-### Production Next Steps
-
-Moving from this proof-of-concept toward a hardened, longer-lived deployment means closing the gaps above. The following are the highest-value extensions, roughly in order of effort and impact.
-
-**True-power measurement** is the biggest step up in capability. Add per-phase voltage sensing via an isolated voltage transformer or a dedicated energy-metering analog front end (AFE) with built-in isolation — **not** a direct resistive divider from mains to the MCU ADC, which lacks the isolation needed for safe mains-connected measurement. From there, implement phase-aligned voltage and current sampling plus a cos(φ) power-factor computation to report actual watts and kWh. One isolated voltage reference per phase is typically sufficient for all circuits on that phase; **the key requirement is that voltage and current samples are time-synchronized** so the power-factor term is meaningful.
-
-**Extended CT range** is the easiest win. For sub-panels with 60 A or 100 A feeder circuits, substitute the SCT-013-060 (60 A / 1 V) or SCT-013-100 (100 A / 1 V) and update `ct_full_scale_amps` via env var — **no firmware change required.**
-
-**Weekend and holiday awareness** sharpens the alerting layer. Pass a site calendar or day-of-week bitmask as an environment variable so the `PLUG_LOAD_ALERTS` business-hours check respects Saturdays, Sundays, and holidays rather than treating every night as identical.
-
-**Over-the-air firmware updates** remove the need for truck rolls. Wire the Cygnet's BOOT/RESET pins to Notecard ATTN for [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/), enabling remote firmware pushes across the entire fleet without revisiting each site.
-
-**Per-circuit calibration offset** tightens accuracy at the low end. Store a small per-circuit zero-current offset in the env vars — measured at commissioning with the circuit breaker open — to subtract each channel's individual noise floor from its reading.
-
-## 13. Summary
-
-The ESCO consultant who used to estimate after-hours plug loads from utility bills and a clipboard now walks into the building, clips four CT clamps onto the sub-panel, taps 120 VAC, and starts collecting per-circuit profiles within the hour — no network form, no VLAN, no IT ticket that ages six weeks. After a few weeks of hourly summaries, the 2 AM `act_min` trend on every circuit makes the answer obvious: the workstation bank that should be off, the signage that runs all night, and the network closet that's been quietly drawing load since the gear it cools was retired.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/commercial-plug-load-after-hours-waste-dashboard/)**
diff --git a/56-commercial-plug-load-after-hours-waste-dashboard/banner.png b/56-commercial-plug-load-after-hours-waste-dashboard/banner.png
deleted file mode 100644
index 01bcbfa7..00000000
Binary files a/56-commercial-plug-load-after-hours-waste-dashboard/banner.png and /dev/null differ
diff --git a/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/01-system-architecture.svg b/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/01-system-architecture.svg
deleted file mode 100644
index f7975d7f..00000000
--- a/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-
diff --git a/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/02-wiring-assembly.svg b/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index f14207b4..00000000
--- a/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,79 +0,0 @@
-
diff --git a/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/03-data-flow.svg b/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/03-data-flow.svg
deleted file mode 100644
index 61d9dc09..00000000
--- a/56-commercial-plug-load-after-hours-waste-dashboard/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/57-propane-lpg-tank-fill-telemetry/README.md b/57-propane-lpg-tank-fill-telemetry/README.md
index 569830ae..c33890c5 100644
--- a/57-propane-lpg-tank-fill-telemetry/README.md
+++ b/57-propane-lpg-tank-fill-telemetry/README.md
@@ -1,583 +1,5 @@
# Propane / LPG Tank Fill Telemetry
-
+A reference design giving propane dealers per-tank fill telemetry across their delivery territory, enabling demand-driven dispatch and days-until-empty projections over cellular or satellite.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Propane%20%2F%20LPG%20Tank%20Fill%20Telemetry%20%E2%80%94%204%E2%80%9320%20mA%20Float-Transmitter%20Variant).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/propane_tank_telemetry/](firmware/propane_tank_telemetry/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [truck roll reduction](https://blues.com/truck-roll-reduction/) reference design that gives propane dealers per-tank fill telemetry across their entire delivery territory, replacing fixed-schedule routes with demand-driven dispatch and projecting days-until-empty for every tank in the fleet. A level sensor at the tank's existing gauge port and a temperature probe on the tank shell turn each tank into a self-reporting asset; a single [Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) (one module carrying cellular, WiFi, and Skylo satellite radios that fails over between them automatically) carries the data from the tank three minutes outside town just as readily as from the remote mountain cabin beyond any cell tower. One SKU and one firmware image cover every site type in the territory.
-
-## 1. Project Overview
-
-**The problem.** Most propane dealers still run their delivery routes on a fixed calendar: every six weeks, every tank gets a truck. That schedule exists not because it matches demand, but because dealers have no way to know which tanks actually need filling. The result is trucks rolling to tanks that are at 60% and tanks at remote farm properties that run dry between scheduled visits. Neither failure is exotic; they're structural consequences of not having fill-level data.
-
-The root cause is infrastructure. A propane tank sits in a field, on a farm, at a cabin, at a rural business, almost never near a WiFi access point the dealer can use, and often in areas where cellular is marginal to nonexistent. The tank itself is a sealed pressure vessel with no native communication capability. Retrofitting telemetry means solving a connectivity problem that varies by site, a sensor problem specific to LP (liquefied petroleum) gas vessels, and a data problem of turning raw fill readings into actionable dispatch intelligence.
-
-This project solves all three. **A weatherproof electronics enclosure mounted on a post or bracket outside the AHJ-defined classified area**, connected by field wiring to a 4-20 mA LP gauge-port float transmitter at the tank's existing dip-tube gauge port and a DS18B20 temperature probe strapped to the tank shell. The device wakes every 15 minutes, reads the transmitter current and tank temperature, converts the transmitter's linear 4–20 mA output directly to fill percentage, updates a smoothed daily consumption rate, and reports a daily summary to the [Blues Notehub](https://blues.com/notehub/) cloud service. When fill drops below a configurable low-fill threshold, an alert fires immediately. The dealer's dispatch system sees fill %, gallons remaining, and projected days-until-empty for every tank in the fleet. That's enough to replace the calendar with a demand-driven route that only rolls a truck when a tank actually needs it.
-
-**Why Notecard for Skylo.** Propane tanks are at farms, cabins, rural businesses, and residential properties — most with no customer WiFi the dealer can use, and many in areas where cellular coverage is spotty to nonexistent. A dealer network spans all of these site types and can't afford a different hardware solution for each one. [Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) (NOTE-NBGLWX) carries three radios on one M.2 module — cellular (LTE-M / NB-IoT / GPRS), WiFi, and satellite over the [Skylo](https://www.skylo.tech/) non-terrestrial network (NTN) — and selects among them automatically. The firmware sets a single [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) preference of `wifi-cell-ntn`: prefer WiFi where a provisioned AP happens to be in range (the rare residential tank), fall back to cellular (the de-facto primary across the bulk of the territory), and fall back again to Skylo satellite at remote sites. Failover is handled by the Notecard; the host firmware never branches on which network is live. That collapses what used to be a two-device decision — a cellular Notecard for in-coverage tanks plus a separate satellite add-on for the rest — into a single part number, a single antenna kit, and a single firmware image that deploys unchanged across the entire fleet. Notecard for Skylo ships with an active global SIM including 500 MB of cellular data and 10 years of service, plus 10 KB of bundled Skylo satellite data, with no activation fees and no monthly per-SIM commitment. There is nothing to swap when a site turns out to have weaker coverage than the survey suggested: the same board that runs on cellular near town automatically reaches the Skylo network at the cabin.
-
-
-
-**Deployment scenario.** A weatherproof NEMA 4X enclosure mounted on a separate post, wall, or bracket **outside the AHJ-defined classified area** (see the safety notice in §4), powered by a solar-charged 12 V sealed lead-acid battery. A 4-20 mA LP gauge-port float transmitter (Rochester Sensors M6300-LP Magnetel® gauge + R6315-12 transmitter, or equivalent) connects at the tank's existing 1¼″ NPT dip-tube gauge port; field wiring from the transmitter runs to the electronics enclosure outside the hazardous boundary. A waterproof DS18B20 temperature probe is clamped to the tank shell and logged in daily summary Notes for seasonal demand analytics. Notecard for Skylo's antenna cables exit the enclosure to outdoor-mounted antennas with a clear sky view. The same Skylo-certified antenna carries both cellular and satellite, so where cellular coverage is absent the board falls back to the Skylo NTN satellite network automatically over the same Notehub project, with no antenna swap and no firmware changes. No modifications to the tank itself, no on-site internet infrastructure, and no OEM cooperation required.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Inside the post-mounted enclosure, the Cygnet STM32 host on the Notecarrier CX wakes every `sample_interval_min` (default 15 minutes), reads the 4-20 mA float transmitter and the DS18B20 shell probe, converts the linear current straight to fill percentage and gallons remaining, and updates a smoothed consumption-rate estimate. If a threshold trips, it queues an alert Note right then; otherwise it just goes back to sleep. Once per `report_interval_hr` (default 24 hours) it ships a templated summary carrying current fill %, fill gallons, minimum fill seen in the window, window-averaged temperature, daily consumption rate, and projected days-until-empty. Between wakes the host is cut entirely via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) and Notecard for Skylo idles at ~8 µA. Every queued [Note](https://dev.blues.io/api-reference/glossary/#note) travels over I²C with no JSON hand-rolling and no AT commands.
-
-**Notecard responsibilities.** Notecard for Skylo runs the same playbook regardless of which radio is live: hold Notes in its on-device queue, open a session on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence, and push any `sync:true` alert through immediately. The `card.transport` `wifi-cell-ntn` preference set at boot decides the path automatically — WiFi where a provisioned AP is reachable, cellular at the bulk of sites, and Skylo satellite at remote sites — with no firmware branching. Either way, the same module pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from Notehub on every inbound sync, so a dispatcher can retune transmitter calibration or alert thresholds for any tank in the fleet without anyone driving out to it.
-
-**Notehub responsibilities.** Events land in [Notehub](https://dev.blues.io/notehub/notehub-walkthrough/) from whatever transport carried them, where they're ingested, stored, and fanned out through project-level routes. Per-fleet [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) are how a single firmware image services tanks of every capacity: the tank size and sensor calibration live in Notehub, not in compiled constants. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) make it easy to group tanks by territory, capacity class, or customer type so the right calibration values flow to the right tanks automatically.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — this reference design does not ship any specific downstream endpoint. The natural downstream targets for a propane dealer are a route-optimization or ERP system (routed via HTTP webhook) and a time-series database for trend analysis (routed to a cloud data store of the dealer's choosing).
-
-## 3. Technical Summary
-
-Clone this repository, build the firmware, and deploy:
-
-1. **Get the firmware onto your Notecarrier CX:**
- ```
- arduino-cli core install STM32:stm32
- arduino-cli lib install "Blues Wireless Notecard" OneWire DallasTemperature
- arduino-cli compile -b STM32:stm32:Nucleo_L476RG firmware/propane_tank_telemetry/
- arduino-cli upload -b STM32:stm32:Nucleo_L476RG -p /dev/ttyACM0 firmware/propane_tank_telemetry/
- ```
- Adjust the port (`/dev/ttyACM0` on Linux/Mac, `COM*` on Windows) and board as needed.
-
-2. **Claim your Notecard to Notehub:**
- Sign up at [notehub.io](https://notehub.io) and create a project. Copy the ProductUID and paste it into the firmware as `PRODUCT_UID`.
-
-3. **Configure fleet variables in Notehub** — Set these in **Projects → Environment (tab)** at the Fleet level:
- - `tank_capacity_gal`: your tank's usable capacity (e.g., 500)
- - `fill_alert_pct`: alert threshold (default 20)
- - All others have sensible defaults; see [§6](#6-notehub-setup) for the full list.
-
-4. **See your first event:**
- After a few minutes of network registration, check **Notehub → Devices → [Your device] → Events**. You should see a `tank_status.qo` daily summary Note (or a `tank_alert.qo` if your tank is below the alert threshold). The Note body carries `fill_pct`, `fill_gal`, `temp_c`, `gal_per_day`, and `days_until_empty`.
-
-Full assembly and commissioning instructions follow in §4–§9.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "tank_status.qo",
- "body": {
- "fill_pct": 42.3,
- "fill_gal": 211.5,
- "min_fill_pct": 38.1,
- "temp_c": 14.8,
- "gal_per_day": 8.2,
- "days_until_empty": 25.8,
- "transmitter_ma": 11.3
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with onboard Cygnet STM32 host — handles the 12-bit ADC for the 4-20 mA loop and OneWire for the temperature probe with no external MCU needed. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) · [datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | 1 | One M.2 module carrying cellular (LTE-M / NB-IoT / GPRS, Quectel BG95-S5 modem), WiFi (Silicon Labs WFM200S), and Skylo satellite (NTN) radios. The firmware's `card.transport` `wifi-cell-ntn` setting makes it prefer cellular at in-coverage tanks and fall back automatically to the Skylo satellite network at remote sites beyond cellular reach — no second device or part-number decision. Cellular removes the per-site IT dependency; the embedded global SIM and bundled satellite allocation cover the whole territory from one SKU. Requires the antennas below. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | **Bench only.** Coulomb counter on the 5 V supply rail for validating power behavior during commissioning (see §9). Remove and discard for field deployment; not integrated into production units. |
-| Rochester Sensors M6300-LP Magnetel® Rough Rider® LP gas gauge + R6315-12 4-20 mA transmitter — [M6300-LP datasheet](https://rochestergauges.com/wp-content/uploads/M6300-LP.pdf) · [R6315-12 datasheet](https://rochestersensors.com/wp-content/uploads/R6315-12-July-8.pdf) · [6300 series installation guide](https://rochestersensors.com/wp-content/uploads/6300_Magnetel_Gauge.pdf). Available from [John M. Ellsworth Co.](https://www.jmesales.com/rochester-gauge-magnetel-4-20-ma-liquid-level-transmitter/) and LP gas equipment distributors. | 1 | Float-type magnetel level transmitter designed specifically for LP gas service. A float rides the liquid propane surface inside the tank and drives a two-wire 4-20 mA current loop proportional to fill level: 4 mA at 0 % fill, 20 mA at 100 % fill. **Representative orderable assembly:** M6300-LP gauge body (1¼″ NPT, 4″ dial) with R6315-12 transmitter module attached — the R6315-12 clips to the M6300 gauge body and provides the 4-20 mA output. The R6315-12 outputs 4-20 mA proportional to the float position; see the R6315-12 datasheet for electrical specifications and wiring. **Mounting:** the M6300-LP installs at the tank's existing 1¼″ NPT dip-tube gauge port (the standard gauge opening on most horizontal residential and commercial LP tanks). Dip-tube length and float arm length vary by tank geometry — confirm the correct M6300-LP model suffix against the Rochester Sensors product application table and the tank's nameplate before ordering. Some larger commercial tanks use a 2″ NPT gauge boss; Rochester Sensors offers corresponding variants. **Electrical:** two-wire loop-powered, 12–28 V DC supply. **Note:** because the float tracks the liquid surface directly, no density or temperature correction is applied in firmware — the 4-20 mA output is already proportional to fill level regardless of liquid temperature or density. Installation at a propane pressure vessel must be performed by a licensed LP gas technician per NFPA 58. |
-| [SparkFun Waterproof DS18B20 Temperature Sensor (SEN-11050)](https://www.sparkfun.com/products/11050) | 1 | OneWire temperature probe clamped to the tank shell. Included in daily summary Notes for cloud-side consumption correlation against ambient conditions. |
-| 120 Ω 0.1 % precision resistor | 1 | Shunt across the 4-20 mA current loop. Converts 4–20 mA to 0.48–2.40 V DC at the Cygnet's A0 ADC pin. The 120 Ω value provides safe electrical headroom for standard 4–20 mA transmitter fault currents: a 24 mA diagnostic output produces only 2.88 V at A0 — well within the 3.3 V ADC absolute maximum. The firmware rejects currents outside the 3.5–21 mA valid window as `NAN` and emits a `sensor_fault` alert; with this shunt the ADC is electrically safe up to ~27 mA (3.3 V ÷ 120 Ω). |
-| 4.7 kΩ resistor | 1 | OneWire pull-up for the DS18B20 data line. |
-| 12 V DC/DC step-down module, 5 V / 2.5 A output (e.g. [Pololu D24V22F5](https://www.pololu.com/product/2858)) | 1 | Steps 12 V battery rail down to 5 V for the Notecarrier CX VBAT input. A 2.5 A continuous output rating is required for this design: Notecard for Skylo's onboard modem (Quectel BG95-S5) can momentarily demand up to ~2 A in brief bursts, and a 1 A regulator cannot reliably source that peak. The D24V22F5 delivers 5 V at up to 2.5 A continuous from a 4.5–42 V input, giving comfortable headroom above the Notecard's 2 A burst demand plus the Cygnet host's active-mode draw. |
-| 24 V DC/DC boost converter module, input 8–16 V, output regulated 24 V, ≥ 100 mA (e.g. [Pololu U3V50F24](https://www.pololu.com/product/2569)) | 1 | Provides a stable, regulated 24 V supply for the 4–20 mA transmitter current loop. A regulated 24 V loop supply guarantees transmitter compliance across the full SLA discharge cycle, any practical cable run, and an AHJ-required IS barrier. See the loop compliance calculation in §5 Step 7. **Do not drive the transmitter loop from the 12 V system rail**: at battery sag (11–11.5 V under partial charge and load) the loop supply falls below the R6315-12's 12 V minimum specification, and any IS barrier in the loop worsens the margin further. The Pololu U3V50F24 accepts 2–16 V input and delivers a regulated 24 V at up to 500 mA; it is powered from the same 12 V system rail as the 5 V step-down module. |
-| Skylo-certified LTE/satellite antenna included with Notecard for Skylo (u.FL) | 1 | Connects to the **MAIN** u.FL port and carries **both** the terrestrial cellular signal and the Skylo satellite link — a single antenna for both networks. Use only the Skylo-certified antenna supplied with Notecard for Skylo; substituting an uncertified antenna risks regulatory non-compliance and link failure. Mount outdoors with an unobstructed view of the sky (northern hemisphere: the southern sky) so the board can reach Skylo wherever it falls back from cellular; route through a cable gland. Do not rely on the bare antenna inside a polycarbonate enclosure at tank-side — signal margin is too variable. |
-| u.FL-to-SMA-F bulkhead pigtail, ~15–20 cm (e.g. Taoglas CAB.0150.A.01 or equivalent) | 0–1 | **Optional.** Only needed to mount an external SMA-terminated antenna instead of routing the bare u.FL antenna through the enclosure wall. Adapts the MAIN u.FL port to an external SMA bulkhead, then connects to the external antenna cable outside. Not required when the included Skylo-certified antenna is routed directly through a gland. |
-| Passive GPS/GNSS antenna (u.FL) per the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | 1 | Connects to the **GPS** u.FL port for GNSS time/location. Mount outdoors with a clear sky view alongside the main antenna; route through a cable gland. |
-| IP68-rated PG-11 nylon cable gland, 5–10 mm cable OD (e.g. Lapp SKINTOP® MS-M PG 11 or equivalent) | 4 | One gland per field cable entering the enclosure. This installation requires penetrations for: **(1)** the Skylo-certified MAIN antenna lead (or its SMA bulkhead); **(2)** the GPS/GNSS antenna lead; **(3)** DS18B20 temperature probe cable; and **(4)** 4–20 mA transmitter loop wiring (two conductors from transmitter to enclosure). Every penetration must be sealed to maintain the NEMA 4X enclosure rating. Select gland cable-OD range to match each specific cable's outer diameter. |
-| 12 V sealed lead-acid battery, 7–10 Ah (e.g. Universal Power Group UB1280, 8 Ah) | 1 | Energy reservoir for cloudy periods and overnight. At this device's duty cycle, 8 Ah provides ≥3 days of autonomy without solar input (see [Limitations](#11-limitations-and-next-steps) for power budget). |
-| Inline fuse holder + 5 A automotive blade fuse (e.g. Bussmann BP/HHM + ATC5) | 1 | Placed in the battery (+) lead as close to the battery positive terminal as possible. Protects wiring from a short-circuit fault in the load circuit. The 5 A rating provides adequate protection for the ≤2 A maximum load while giving headroom above the controller's worst-case startup surge. Use a weatherproof fuse holder if the battery is outside the NEMA 4X enclosure. |
-| [Victron SmartSolar MPPT 75/10](https://www.victronenergy.com/solar-charge-controllers/smartsolar-mppt-75-10) solar charge controller | 1 | Regulates solar panel output to charge the 12 V SLA battery safely. The SmartSolar MPPT 75/10 exposes dedicated **LOAD+ / LOAD−** output terminals with built-in low-voltage disconnect (LVD) that automatically cuts load power to protect the SLA from deep discharge — this specific feature is required for the wiring in §5. If a different controller is substituted, confirm it has equivalent LOAD terminals with LVD before following the §5 wiring steps. |
-| Monocrystalline solar panel, 10–20 W, 12 V nominal (e.g. Renogy 10W Monocrystalline RNG-10D-SS) | 1 | 10 W is adequate for mid-latitude sites (≥4 h/day usable sun). Size up to 20 W for northern latitudes, winter deployments, or sites with frequent cloud cover. Mount with a clear sky view, not against the tank body. |
-| NEMA 4X polycarbonate enclosure, ~8 × 6 × 3.5″ | 1 | Weatherproof housing rated for outdoor tank-side mounting. Polycarbonate is UV-resistant and non-sparking. |
-
-**Bundled connectivity (Notecard for Skylo):** Ships with an active global SIM including 500 MB of cellular data and 10 years of service, **plus** 10 KB of bundled Skylo satellite data — no activation fees, no monthly commitment, and no separate satellite provider subscription. Additional satellite data is billed per byte (see the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for current pricing). Minimizing inbound sync frequency conserves the bundled satellite allocation.
-
-
-
-**Safety notice.** LP gas (liquefied petroleum gas) is a flammable compressed gas. Any sensor connection to a propane tank, including at the gauge port — must be performed by a qualified LP gas technician following applicable codes (NFPA 58, CGA, and local authority having jurisdiction). The level transmitter, fittings, and wiring must be rated for LP gas service.
-
-**Classified-area / hazardous-location requirement.** Blues Notecard and Notecarrier CX electronics are **not** rated for hazardous locations (NEC Class I Division 1 or Division 2 / ATEX Zone equivalents) and must **not** be installed inside the classified area as defined by the installation's authority having jurisdiction (AHJ) and NFPA 58. The classified area boundary around a propane container typically extends several feet from fittings, relief valves, and regulators — confirm the boundary with the AHJ and the installing LP gas technician before positioning the enclosure. Mount the electronics enclosure outside that boundary. For 4–20 mA transmitter loop wiring that crosses the classified-area boundary, the AHJ may require a listed intrinsic-safety (IS) barrier or other approved interface device in the loop circuit, between the tank-side transmitter and the enclosure-side electronics. Confirm IS barrier requirements with the installing LP gas technician and the AHJ before wiring.
-
-
-
-## 5. Wiring and Assembly
-
-
-
-All Notecarrier CX host I/O lands on its dual 16-pin header. Notecard for Skylo seats into the M.2 slot. The Mojo sits inline between the 5 V step-down and the Notecarrier +VBAT pad during bench validation only; it is not deployed in the field.
-
-
-
-**Enclosure placement first.** Blues Notecard and Notecarrier CX electronics must be installed **outside** the classified-area boundary defined by NFPA 58 and the authority having jurisdiction — confirm that boundary with the installing LP gas technician before drilling cable-gland holes or positioning the enclosure.
-
-
-
-**Step 1: Enclosure and cable-gland assignment.** Install the NEMA 4X polycarbonate enclosure at the chosen mounting location. Drill and install one cable gland for each field-cable penetration before pulling cables. Assign glands as follows:
-
-| Gland | Cable / penetration |
-|---|---|
-| 1 | Skylo-certified MAIN antenna lead (or its SMA bulkhead) — carries cellular and satellite |
-| 2 | GPS/GNSS antenna lead (GPS u.FL port) |
-| 3 | DS18B20 temperature probe cable |
-| 4 | 4–20 mA transmitter loop wiring (2-conductor, from transmitter to enclosure) |
-
-Match each gland's cable-OD clamp range to the actual cable outer diameter. Tighten all glands to the manufacturer's torque spec after routing cables; leave no unused penetration open.
-
-**Step 2: Solar panel to charge controller (PV input).**
-
-- **Solar panel PV+** → charge controller **PV+** terminal.
-- **Solar panel PV−** → charge controller **PV−** terminal.
-
-Mount the panel in an unobstructed location with good sky view — not against the tank body or in the tank's shadow. Tilt angle should maximize winter sun exposure for the deployment latitude. Run the panel cable into the enclosure through an appropriately-sized cable gland (or use an MC4 weatherproof pass-through if the controller has external PV terminals). Keep the run as short as practical to minimize resistive loss.
-
-**Step 3: SLA battery to charge controller (battery terminals).**
-
-- **Battery (+)** → charge controller **BATT+** terminal.
-- **Battery (−)** → charge controller **BATT−** terminal.
-
-Use wire rated for the battery's short-circuit current (14 AWG minimum for a 7–10 Ah SLA). Place the **inline fuse holder + 5 A fuse** from the BOM in the battery (+) lead, as close to the battery positive terminal as possible. Use a weatherproof fuse holder if the battery is positioned outside the NEMA 4X enclosure.
-
-**Step 4: Charge controller load output to step-down module.**
-
-- **Charge controller LOAD+** → step-down module **Vin+**.
-- **Charge controller LOAD−** → step-down module **Vin−** / GND.
-
-The Victron SmartSolar MPPT 75/10 has dedicated **LOAD+ / LOAD−** terminals with built-in low-voltage disconnect (LVD); these terminals switch the 12 V supply on and off based on battery state to protect the SLA. **Only use this wiring with a controller that has LOAD terminals and LVD** — if a different controller is substituted, verify those features before following these steps. If the controller lacks LOAD terminals, wire from BATT terminals and use the controller's LVD relay or alarm output to interrupt the load circuit.
-
-**Step 5: 12 V step-down to Mojo and Notecarrier +VBAT (5 V rail).**
-
-Set the step-down module output to **5.0 V** and verify with a meter before connecting any load.
-
-```
-Step-down Vout+ → Mojo BAT+
-Step-down Vout− → Mojo BAT− / GND
-Mojo LOAD+ → Notecarrier CX +VBAT
-Mojo LOAD− → Notecarrier CX GND
-```
-
-During bench validation the Mojo sits in this path to measure 5 V rail current. For field deployment, remove the Mojo and wire the step-down Vout+ directly to Notecarrier CX +VBAT.
-
-**Step 6: Notecard installation and antenna connections.**
-
-Seat Notecard for Skylo (NOTE-NBGLWX) into the Notecarrier CX M.2 slot and secure the retaining screw.
-
-Notecard for Skylo exposes two antenna u.FL ports — **MAIN** and **GPS**. The single Skylo-certified antenna on the MAIN port carries **both** the cellular and the satellite signal; there is no separate satellite antenna to install. Connect antennas as follows:
-
-- **MAIN port:** attach the included Skylo-certified antenna, routed through Gland 1 (connect it directly, or adapt it to an external SMA bulkhead with a u.FL-to-SMA-F pigtail). Mount the antenna outdoors with an unobstructed view of the sky (northern hemisphere: the southern sky) so the board can reach the Skylo satellite network wherever it falls back from cellular. Use only the Skylo-certified antenna; an uncertified part risks regulatory non-compliance and link failure. *Do not skip this connection — both cellular and satellite operation require this external antenna.*
-- **GPS port:** attach the passive GPS/GNSS antenna, routed through Gland 2, and mount it outdoors with a clear sky view alongside the main antenna.
-
-Select antenna cable lengths to suit the routing distance before ordering — most pre-terminated assemblies should not be cut or field-terminated. Secure any excess cable outside the enclosure with a gentle loop; do not coil cable inside the enclosure.
-
-**Step 7: 4–20 mA current loop (gauge-port level transmitter).**
-
-The Rochester Sensors M6300-LP + R6315-12 is a two-wire loop-powered instrument. A single 2-conductor cable runs from the transmitter (at the tank gauge port, outside or at the hazardous boundary) to the electronics enclosure. Route this cable through Gland 4. Wire the loop from the **dedicated 24 V boost converter output** — not the 12 V system rail and not the 5 V Notecarrier rail (see compliance Note below):
-
-- **24 V boost converter Vin+** → charge controller **LOAD+** (same 12 V rail that feeds the 5 V step-down). **24 V boost converter GND** → **system GND**.
-- **24 V boost converter Vout+** (regulated 24 V) → transmitter **+ terminal** (loop supply in).
-- **Transmitter − terminal** (loop signal out) → one leg of the **120 Ω** precision shunt resistor; other shunt leg → **system GND** (same node as Notecarrier GND and battery −).
-- **A0** (Cygnet analog input on the Notecarrier CX header) → the junction between the transmitter − terminal and the shunt resistor.
-
-At 4 mA (float at bottom — empty tank): V(A0) = **0.48 V**. At 20 mA (float at full-scale — full tank): V(A0) = **2.40 V**. Both are well within the 0–3.3 V Cygnet ADC range. With the 120 Ω shunt, a 24 mA transmitter diagnostic output produces only 2.88 V at A0 — the ADC is electrically safe up to ~27 mA (3.3 V ÷ 120 Ω). The firmware's software fault window is 3.5–21 mA; any current outside that range asserts a `sensor_fault` alert and marks the reading `NAN`. The shunt voltage and ADC range are unchanged from a 12 V supply design — only the loop supply source changes.
-
-**Loop compliance calculation.** The R6315-12 datasheet specifies a supply voltage range of 12–28 V DC. Worst-case compliance budget at 20 mA full-scale:
-
-| Element | Voltage drop at 20 mA |
-|---|---|
-| 120 Ω shunt (sense resistor, enclosure side) | 20 mA × 120 Ω = **2.40 V** |
-| 100 m of 22 AWG field wiring (both conductors combined) | 20 mA × 2 × 100 m × 0.053 Ω/m ≈ **0.21 V** |
-| IS barrier (Zener-diode shunt type, if required by AHJ) | ≤ **2.5 V** at 20 mA |
-| **Total worst-case external drops** | **≤ 5.11 V** |
-
-With the regulated 24 V boost converter output: V at transmitter terminals = 24 V − 5.11 V = **18.89 V** — 6.89 V above the 12 V lower compliance limit, and well below the 28 V upper limit. The transmitter stays in regulation across the full battery charge/discharge cycle, any cable run up to 100 m, and with or without an IS barrier.
-
-**Why not the 12 V system rail?** An SLA battery discharges from ≈ 12.8 V (fully charged) to the low-voltage disconnect threshold (≈ 10.8 V); at partial charge under load it commonly measures 11.0–11.5 V — already at or below the R6315-12's 12 V minimum supply before accounting for cable resistance or an IS barrier. Adding either makes the margin worse still. The 24 V boost converter removes this dependency: it provides a regulated 24 V output as long as the SLA remains above the converter's ≈ 8 V minimum input voltage, which is well below the charge controller's LVD threshold.
-
-The transmitter process connection is at the tank's **single 1¼″ NPT dip-tube gauge port** — no second tank connection is needed. The float mechanism provides the level reference internally. The LP gas technician installs the transmitter at the gauge port and runs the 2-conductor loop cable from the transmitter to the enclosure; confirm the correct dip-tube length for the tank geometry before installation.
-
-Confirm that system GND (battery −), charge controller LOAD−, step-down Vin−, step-down Vout−, boost converter GND, and Notecarrier GND are all connected to the same node before applying power.
-
-**Step 8: DS18B20 temperature probe.**
-
-Route the DS18B20 cable through Gland 3. Mount the probe body against the tank shell — ideally under a layer of closed-cell foam tape to improve thermal contact and reduce ambient-air influence — and secure the cable with a zip tie to prevent mechanical strain at the gland. Inside the enclosure:
-
-- **DS18B20 red wire (VCC)** → **+3V3** on the Notecarrier CX header.
-- **DS18B20 black wire (GND)** → **GND** on the Notecarrier CX header.
-- **DS18B20 yellow wire (data)** → **D2** on the Notecarrier CX header.
-- **4.7 kΩ pull-up resistor** between the data wire and **+3V3**. The Notecarrier CX does not include an onboard OneWire pull-up; this resistor is required.
-
-**Step 9: Satellite operation (automatic, no added hardware).**
-
-No additional module or wiring is required for satellite operation — Notecard for Skylo's satellite radio is on the same M.2 board as cellular and WiFi, and the single Skylo-certified MAIN antenna (Step 6) carries the satellite link. The firmware enables cellular→satellite fallback by setting `card.transport` to `wifi-cell-ntn` at boot (see §7); the Notecard then routes queued Notes over the Skylo NTN satellite network automatically at any site where cellular is unavailable. To make satellite work in the field, the MAIN antenna must be mounted **outdoors with an unobstructed view of the sky** (northern hemisphere: the southern sky) — confirm a suitable mounting location at remote tank sites. The daily summary Note (~40 bytes) fits well within the NTN payload budget. Note that the board must complete at least one initial non-NTN (cellular or WiFi) sync to associate with Notehub and register Notefile templates before satellite can be used; commission each unit where it has terrestrial coverage even if it will routinely operate over satellite. Review the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) for duty-cycle and data-budget considerations before deploying to a cellular-dark site.
-
-**Final pre-power checklist.**
-
-- All cable glands tightened; no unused enclosure penetrations open.
-- All GND nodes common: battery −, charge controller LOAD−, step-down Vin−, step-down Vout−, boost converter GND, Notecarrier GND.
-- Step-down output confirmed at 5.0 V before connecting Notecarrier.
-- Boost converter Vin+ wired to 12 V system rail (LOAD+); boost converter Vout+ confirmed at 24 V before connecting transmitter loop wiring.
-- Skylo-certified MAIN antenna connected and routed outdoors with a clear sky view (carries both cellular and satellite); GPS/GNSS antenna connected to the GPS port and routed outdoors.
-- 120 Ω shunt installed in the current loop with A0 tapped at the transmitter − / shunt junction; loop supply drawn from 24 V boost converter output (not the 12 V rail).
-- Inline fuse installed in battery (+) lead, as close to the battery positive terminal as possible.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into the firmware as `PRODUCT_UID`.
-
-2. **Claim the Notecard.** Power the unit; on first cellular session the Notecard associates with the project automatically. Verify the device appears in the Notehub device list and shows a recent session.
-
-3. **Optional: configure WiFi credentials.** Notecard for Skylo supports WiFi as an optional secondary transport — the device operates normally on cellular (or satellite) alone if no WiFi credentials are set. Two reliable provisioning paths are available for this hardware stack:
-
- - **Preferred: Notehub environment variables (works on deployed hardware).** Set the `wifi_ssid` and `wifi_password` environment variables in Notehub (see the env var table in step 5 below). Env vars are delivered to the device on the next **inbound sync**. Because `hubConfigure()` sets inbound at 2× the outbound period, the default 24-hour report cadence means inbound syncs occur every 48 hours — **WiFi credentials set in Notehub may take up to 48 hours to reach the device.** Once the inbound sync delivers the vars, the firmware issues `card.wifi` to store the credentials on the Notecard; the Notecard then connects over WiFi when it provides better or equivalent coverage to cellular. For faster credential rollout, temporarily lower `report_interval_hr` (e.g. set to 1 hour), confirm connectivity, then restore the original value. Credentials persist on the Notecard even if the env vars are later cleared.
-
- - **Alternative: direct Notecard USB (bench setup before deployment).** Connect a USB cable to the **Notecard module's own USB port** (the small USB connector on the Notecard M.2 card itself — not the Notecarrier CX USB-C port, which connects to the Cygnet host MCU). With that connection active, open [Notecard Playground](https://dev.blues.io/terminal/) and issue: `{"req":"card.wifi","ssid":"","password":""}`. This is most practical to do at the bench before sealing the enclosure for field deployment.
-
- WiFi is not required for normal operation and does not need to be provisioned on every device.
-
-4. **Create a Fleet per territory.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) let you group devices for shared configuration and routing. A natural breakdown is one fleet per delivery territory or per tank capacity class — a 250-gallon residential fleet and a 1000-gallon commercial fleet may have different alert thresholds and report cadences. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can auto-assign new devices by matching tags set at provisioning time (e.g. `tank_size:1000gal`).
-
-5. **Set environment variables.** In Notehub, navigate to **Projects → [Your Project] → Environment** and configure a Fleet at the **Fleet level** or **Device level**. All variables below are optional; firmware compile-time defaults are shown in parentheses. Any value set in Notehub takes effect on the device's next inbound sync — no firmware re-flash required. Best practice: set variables on the Fleet, so a single configuration applies to all tanks in the fleet; override at the Device level for per-tank exceptions.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `tank_capacity_gal` | `500` | Usable tank capacity in gallons at 100 % gauge reading. |
- | `sensor_empty_ma` | `4.0` | Transmitter output current in mA corresponding to 0 % fill (empty tank). Standard 4–20 mA transmitters output exactly 4.0 mA at empty; adjust only if the installed unit has a non-standard live-zero (rare). |
- | `sensor_full_ma` | `20.0` | Transmitter output current in mA corresponding to 100 % fill. Standard 4–20 mA transmitters output 20.0 mA at full; adjust if the installed unit does not reach exactly 20 mA at the full-tank position. |
- | `fill_alert_pct` | `20` | Fill percentage below which a `low_fill` alert fires. |
- | `consumption_alert_gal_per_day` | `100` | Daily consumption rate above which a `high_consumption` alert fires. Useful for detecting leaks or sudden demand spikes. |
- | `sample_interval_min` | `15` | Minutes between sensor samples. |
- | `report_interval_hr` | `24` | Hours between summary Notes. |
- | `alert_cooldown_hr` | `4` | Hours between repeated alerts of the same type. Prevents a slowly-draining tank from generating an alert every 15 minutes. |
- | `consumption_alert_streak` | `3` | Number of consecutive sample cycles above `consumption_alert_gal_per_day` required before a `high_consumption` alert fires. Increasing this value reduces false positives from transient ADC jitter or short-lived post-refill spikes; set to `1` to alert on the first above-threshold reading. |
- | `wifi_ssid` | *(unset)* | SSID of a WiFi network to use as a secondary transport. Both `wifi_ssid` and `wifi_password` must be set; the firmware issues `card.wifi` on the next inbound sync (up to 48 hours at the default cadence. See step 3 above) so the Notecard stores the credentials and connects over WiFi when available. Credentials persist on the Notecard even if the env vars are later cleared. Note: env var values are visible to Notehub project collaborators — use a dedicated IoT or guest network. |
- | `wifi_password` | *(unset)* | WPA2 passphrase for the network specified by `wifi_ssid`. |
-
-6. **Satellite deployments (cellular-dark sites).** No hardware change is needed — Notecard for Skylo's satellite radio is already on the board, and the firmware enables cellular→satellite fallback by setting `card.transport` to `wifi-cell-ntn` (see §7). At sites with no cellular coverage the board falls back to the Skylo NTN satellite network automatically; all events travel through the same Notehub project regardless of transport. The board must complete at least one initial non-NTN (cellular or WiFi) sync to associate with the project and register Notefile templates before satellite can be used, so commission each unit where it has terrestrial coverage even if it will routinely operate over satellite. The daily summary Note (~40 bytes per record) fits within typical NTN satellite payload budgets; each inbound sync also consumes some of the bundled 10 KB satellite allocation, so consider widening `report_interval_hr` (which also widens the inbound cadence) at satellite sites. Review the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) before deploying to understand satellite-specific duty-cycle and data-budget considerations.
-
-7. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `tank_alert.qo` (real-time delivery to the dealer's dispatch or routing system) and a second for `tank_status.qo` (batched delivery to a time-series store for historical trend analysis and route-density modeling). Keeping the two Notefiles separate at the source means each can be fanned out to a different destination at a different urgency — alerts go somewhere that pages an operator; daily summaries go somewhere that feeds a dashboard.
-
-## 7. Firmware Design
-
-Main sketch: [`firmware/propane_tank_telemetry/propane_tank_telemetry.ino`](firmware/propane_tank_telemetry/propane_tank_telemetry.ino). Sensor math, fill-level calculation, and consumption tracking are factored into [`firmware/propane_tank_telemetry/propane_tank_telemetry_helpers.h`](firmware/propane_tank_telemetry/propane_tank_telemetry_helpers.h).
-
-Dependencies:
-- **Arduino core for STM32** ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)) — install via Boards Manager.
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (the `note-arduino` library). Install via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`. See the [note-arduino releases](https://github.com/blues/note-arduino/releases) for available versions.
-- [`OneWire`](https://github.com/PaulStoffregen/OneWire) — install via Library Manager.
-- [`DallasTemperature`](https://github.com/milesburton/Arduino-Temperature-Control-Library) — install via Library Manager.
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard configuration (`hub.set`, `card.transport` `wifi-cell-ntn` for cellular→satellite fallback, templates) | `hubConfigure`, `defineTemplates` |
-| Environment-variable fetch per wake | `fetchEnvOverrides` |
-| 4-20 mA loop read → mA current | `readTransmitterMA` |
-| DS18B20 OneWire temperature read | `readTemperatureC` |
-| Transmitter current → fill % (linear interpolation) | `computeFillPct` |
-| Fill % → gallons (linear scale) | `computeFillGal` |
-| Consumption rate (EWMA), days-until-empty projection | `updateConsumption`, `daysUntilEmpty` |
-| Alert evaluation and emission | `runSampleCycle`, `sendAlert` |
-| Daily summary | `sendSummary` |
-| Persistent state across deep-sleep cycles | `PersistState` + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### Sensor reading strategy
-
-**Gauge-port level transmitter (4-20 mA).** The Cygnet's 12-bit ADC reads the voltage across the 120 Ω shunt. A 16-sample average filters switching noise, then the mean voltage is converted to current (mA):
-
-```
-current_ma = (adc_voltage / 120.0) × 1000.0
-```
-
-The firmware's valid window is 3.5–21 mA: below 3.5 mA indicates an open-loop wiring fault; above 21 mA indicates a short or unexpected diagnostic output. Both conditions set the reading to `NAN` and emit a `sensor_fault` alert. With the 120 Ω shunt, a 21 mA signal produces 2.52 V at A0 — well within ADC range. The ADC is electrically safe up to ~27 mA, so a transmitter in full fault-high condition does not risk ADC damage.
-
-**Fill percentage and fill gallons.** The Rochester Sensors M6300-LP + R6315-12 float transmitter outputs a current proportional to the float's position in the liquid propane — the output already represents fill level directly, independent of liquid density or temperature. `computeFillPct` converts the transmitter current to fill percentage with a simple linear interpolation between the configured empty and full calibration points:
-
-```
-fill_pct = (current_ma - sensor_empty_ma) / (sensor_full_ma - sensor_empty_ma) × 100.0
-```
-
-`computeFillGal` then scales that to gallons:
-
-```
-fill_gal = (fill_pct / 100) × tank_capacity_gal
-```
-
-No density or temperature correction is applied to the fill reading — the float tracks the liquid surface directly and its current output is already an accurate representation of fill level across the full operating temperature range.
-
-**Temperature (DS18B20 OneWire).** The DallasTemperature library handles conversion timing. Tank wall temperature is included in daily summary Notes so cloud-side analytics can correlate demand patterns against ambient conditions — propane consumption is strongly seasonal. A missing or faulted DS18B20 produces the `-9999` sentinel in the `temp_c` field of summary Notes but does not affect the fill reading.
-
-### Sensor selection rationale
-
-The initial project specification called for a low-power ultrasonic or pressure level sensor with the DS18B20 temperature probe used for vapor-pressure compensation in the fill measurement path. After evaluating both sensing methods for LP gas tank duty, this design departs from that specification and uses a float-type 4–20 mA transmitter instead. The rationale is detailed in the table below and summarized in the firmware's top-of-file sensor model comment.
-
-| Approach | Why evaluated | Why not selected for this design |
-|---|---|---|
-| **Ultrasonic time-of-flight** (transducer at tank top; measures vapor-space height by round-trip echo time) | Non-contact; no liquid exposure for the sensor electronics; well-established for above-ground tank gauging in other applications | Requires a dedicated transducer port at the tank top — an additional tank penetration beyond the standard 1¼″ NPT gauge port that the Rochester Sensors float gauge already occupies. The speed of sound in the LP vapor space varies with temperature and gas composition; an accurate fill reading requires a temperature-compensated speed-of-sound correction, placing the DS18B20 in the fill measurement path and adding a second independent failure mode. LP vapor composition (propane/butane ratio) is not known to the firmware and varies by supplier and season, introducing a systematic bias that cannot be corrected without a composition sensor. |
-| **Hydrostatic pressure** (pressure sensor measuring liquid column head pressure) | Single-point measurement; sensor can be installed at an existing tank fitting | For a horizontally-oriented LP tank, the gauge port at the top of the tank dome accesses the vapor space, not the liquid column bottom, so a sensor at the gauge port reads vapor pressure, not liquid head. Accessing the liquid bottom requires a second tank penetration (a service-valve tee or a dedicated bottom fitting), which increases installation complexity and the number of tank connections requiring LP gas technician work. Even with a bottom connection, the liquid head for a typical 500-gallon horizontal tank is only ~5 kPa (~0.7 psi) riding on top of ~850 kPa (~120 psi) of vapor pressure; resolving fill level to ±5 % from that signal requires a pressure transmitter with ≤ 0.01 % full-scale accuracy — well beyond standard industrial grade. Alternatively, a differential pressure sensor spanning a bottom port and a vapor-space port cancels the vapor background and improves accuracy, but adds a second tank connection and requires two separate field-wiring runs to the enclosure. Either pressure variant places the DS18B20 in the fill calculation path (density correction), consistent with the original brief, but at the cost of two tank penetrations vs. one. |
-| **Float-type 4–20 mA transmitter** (Rochester Sensors M6300-LP + R6315-12, selected design) | Industry-standard LP gas gauging method; well-understood installation at the existing gauge port | — |
-
-The Rochester Sensors M6300-LP Magnetel® float gauge + R6315-12 transmitter was selected for this design because it installs at the tank's single standard 1¼″ NPT dip-tube gauge port, requires no additional tank penetrations, and produces a 4–20 mA output that is directly proportional to fill level regardless of liquid temperature or density — the float tracks the physical liquid propane surface, so its current output is inherently density-independent. No temperature correction is needed or applied in firmware. The principal trade-off relative to the original specification is a continuously-powered 4–20 mA current loop (the dominant load in the power budget), versus the switched-power or ultra-low-power sensing that an I²C ultrasonic or differential-pressure sensor could provide; see [§11](#11-limitations-and-next-steps) for the power-budget discussion.
-
-The DS18B20 temperature probe is retained in the design for cloud-side seasonal demand analytics. Its reading is included in every `tank_status.qo` daily summary Note and is **not** passed to the fill calculation. The top-of-file sensor model comment in [`propane_tank_telemetry.ino`](firmware/propane_tank_telemetry/propane_tank_telemetry.ino) records this same rationale alongside the implementation.
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates store Notes as compact fixed-length records rather than free-form JSON — at 365 summary Notes per tank per year across a fleet of thousands of tanks, the bandwidth savings are material on a prepaid SIM.
-
-`tank_status.qo` (daily summary):
-
-```json
-{
- "file": "tank_status.qo",
- "body": {
- "fill_pct": 42.3,
- "fill_gal": 211.5,
- "min_fill_pct": 38.1,
- "temp_c": 14.8,
- "gal_per_day": 8.2,
- "days_until_empty": 25.8,
- "transmitter_ma": 11.3
- }
-}
-```
-
-`tank_alert.qo` (immediate, `sync:true`):
-
-```json
-{
- "file": "tank_alert.qo",
- "sync": true,
- "body": {
- "alert": "low_fill",
- "fill_pct": 18.2,
- "fill_gal": 91.0,
- "days_until_empty": 11.1,
- "gal_per_day": 8.2
- }
-}
-```
-
-Two alert types are defined: `low_fill` (fill percentage dropped below `fill_alert_pct`) and `high_consumption` (smoothed daily consumption rate has exceeded `consumption_alert_gal_per_day` for `consumption_alert_streak` consecutive sample cycles, default 3, to debounce transient spikes from ADC jitter or post-refill noise). A third synthetic type, `sensor_fault`, fires when the transmitter current is outside the valid 4-20 mA window — open-circuit or short on the current loop. All three use the same `tank_alert.qo` Notefile with `sync:true` so they bypass the daily outbound window and arrive at the dealer's dispatch system within the Notecard's session-establishment window (typically well under 60 seconds in good cellular coverage, and minutes when the unit is operating over the Skylo satellite link; see the [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for authoritative figures).
-
-
-
-**Satellite alert latency.** When Notecard for Skylo is operating over the satellite (NTN) link, `sync:true` does not eliminate latency — the alert must wait for an available satellite pass, and Skylo's network duty-cycle rules constrain how often sessions can be opened. Treat satellite alert latency as **minutes**, not seconds. The session energy profile over satellite also differs from a cellular session; the cellular Mojo current figures in §9 do not directly apply to satellite sessions. Validate alert timing and solar/battery sizing separately for sites expected to operate over satellite, using the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) before commissioning.
-
-
-
-### Low-power strategy
-
-Between samples, the Cygnet is cut entirely. `loop()` runs one sample cycle, then serializes state into the Notecard's flash via `NotePayloadSaveAndSleep`, which internally calls [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to cut host power for `sample_interval_min × 60` seconds. Notecard for Skylo itself idles at ~8 µA between network sessions (NOTE-NBGLWX published idle figure), regardless of which radio it last used. On the next scheduled wake the host enters `setup()`, calls `NotePayloadRetrieveAfterSleep` to rehydrate state (including the running consumption history and alert cooldown timestamps), then hands off to `loop()` for the next sample cycle.
-
-This structure matters beyond battery life. Because sampling lives in `loop()`, the same code path runs on every iteration — both after a hardware power-cut (deep-sleep field mode) and after the bench `delay()` fallback (USB-powered mode). The behavior you observe on the bench is the behavior you'll see in the field.
-
-The Notecard is configured in `periodic` mode with an `outbound` period matching `report_interval_hr`. Summary Notes accumulate in the Notecard's queue and sync in a single cellular session once per day. Alert Notes set `sync:true` and flush within minutes of triggering.
-
-### Retry and error handling
-
-- The first Notecard transaction at boot uses `notecard.sendRequestWithRetry(req, 10)` to paper over the cold-boot I²C race documented in the note-arduino library.
-- Transmitter current readings outside 3.5–21 mA are rejected as `NAN` and excluded from summary averages. A `sensor_fault` alert fires on the first detection and is suppressed for `alert_cooldown_hr` thereafter — enough to page the dealer once without flooding on every 15-minute cycle.
-- DS18B20 returns `DEVICE_DISCONNECTED_C` (−127.0) if no sensor is present. Firmware checks for this sentinel and excludes the reading from the window temperature average (the `temp_c` field in the summary Note carries the `-9999` sentinel when no valid temperature samples were collected), so a missing probe does not affect the fill reading.
-- Consumption rate is computed only when two consecutive valid fill readings exist. A refill event (fill level increases by more than 5 % between samples) resets the short-term accumulator without touching the EWMA, so a delivery visit doesn't corrupt the running consumption estimate.
-- Env var changes to `report_interval_hr` re-apply `hub.set` on the next wake so the Notecard's outbound cellular cadence stays synchronized with the local summary cadence.
-
-### Key code snippet 1: template definition
-
-Templates compress daily summary Notes to fixed-length records. Notecard template format codes follow the pattern `XY` where `X` is the byte size and `Y` is the type:
-- `14.1`: 4-byte float (±3.4e38, ~7 significant digits, suitable for fill %, consumption rates, days-until-empty)
-- `12`: 2-byte signed integer (−32,768 to 32,767, not used in this design but shown for reference)
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "tank_status.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "fill_pct", 14.1); // current fill at time of note
-JAddNumberToObject(body, "fill_gal", 14.1); // current fill gallons
-JAddNumberToObject(body, "min_fill_pct", 14.1); // lowest fill seen this window
-JAddNumberToObject(body, "temp_c", 14.1); // window-averaged tank temperature
-JAddNumberToObject(body, "gal_per_day", 14.1);
-JAddNumberToObject(body, "days_until_empty", 14.1);
-JAddNumberToObject(body, "transmitter_ma", 14.1); // current transmitter output current
-notecard.sendRequest(req);
-```
-
-For a detailed reference on Notecard template codes, see the [Note.template API documentation](https://dev.blues.io/api-reference/notecard-api/note-requests/#note-template).
-
-### Key code snippet 2: 4-20 mA level transmitter current → fill percentage → fill gallons
-
-`computeFillPct` converts the float-type LP gauge-port transmitter current to fill percentage via a linear interpolation between the configured empty and full calibration points. `computeFillGal` scales that to gallons. No density or temperature correction is applied — the float rides the liquid surface directly, so its current output is already an accurate fill reading across the full operating temperature range.
-
-```cpp
-// In propane_tank_telemetry_helpers.h
-
-// Convert 4-20 mA float-type level transmitter current to fill percentage (0–100 %).
-// The Rochester Sensors M6300-LP + R6315-12 outputs a current proportional to the
-// float position (liquid surface level) inside the tank. A linear interpolation
-// between the configured empty and full calibration currents gives fill percentage
-// directly — no density or temperature correction is needed.
-static float computeFillPct(float current_ma,
- float sensor_empty_ma, float sensor_full_ma) {
- if (isnan(current_ma)) return NAN;
- float span = sensor_full_ma - sensor_empty_ma;
- if (span < 0.1f) return NAN;
- float fill_pct = (current_ma - sensor_empty_ma) / span * 100.0f;
- return fmaxf(0.0f, fminf(100.0f, fill_pct));
-}
-
-// Fill percentage → gallons (linear scale).
-static float computeFillGal(float fill_pct, float tank_capacity_gal) {
- if (isnan(fill_pct)) return NAN;
- return (fill_pct / 100.0f) * tank_capacity_gal;
-}
-```
-
-In `runSampleCycle()` the call is:
-
-```cpp
-float fill_pct = computeFillPct(xmtr_ma, SENSOR_EMPTY_MA, SENSOR_FULL_MA);
-```
-
-Temperature is still read on every cycle (for inclusion in the daily summary) but is not passed to the fill calculation.
-
-### Key code snippet 3: sleep with state persistence
-
-`NotePayloadSaveAndSleep` serializes the runtime state struct into Notecard flash and then calls `card.attn` to cut host power for exactly `SAMPLE_INTERVAL_MIN × 60` seconds. The Notecard's ATTN pin brings the host back up on schedule; `NotePayloadRetrieveAfterSleep` rehydrates the struct at the top of the next `setup()`. Sampling runs in `loop()` before the sleep call so it executes on every iteration — both after a hardware power-cycle (deep-sleep mode) and after the bench `delay()` fallback (USB-powered mode).
-
-```cpp
-// In loop() — sample, then sleep (or delay on bench); repeats each iteration
-runSampleCycle();
-state.cycles++;
-
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, (uint32_t)SAMPLE_INTERVAL_MIN * 60UL, NULL);
-
-// Bench/USB fallback: if card.attn doesn't cut VBAT, wait here, then loop()
-// runs again and performs the next sample — correct cadence with no extra code.
-delay((uint32_t)SAMPLE_INTERVAL_MIN * 60UL * 1000UL);
-```
-
-## 8. Data Flow
-
-
-
-Every `sample_interval_min` (default 15 min) the Cygnet wakes, reads the LP gauge-port level transmitter and DS18B20 temperature probe, converts the transmitter current directly to fill %, updates the consumption EWMA, and evaluates three alert conditions.
-
-**Collected.** Fill percentage (from 4-20 mA float-type level transmitter current, linearly interpolated between the configured empty and full calibration points), fill gallons (fill % × tank capacity), tank shell temperature (DS18B20 °C — included in every summary for seasonal demand analytics), raw transmitter current (mA for diagnostics), smoothed daily consumption rate (gal/day), projected days-until-empty.
-
-**Transmitted.**
-- `tank_status.qo` — one templated Note per `report_interval_hr` (default daily). Carries the **current** (most-recent valid reading) `fill_pct`, `fill_gal`, and `transmitter_ma` — not window averages, so a refill or sharp draw-down is accurately represented at reporting time. Also carries `min_fill_pct` (the lowest fill percentage seen during the window, for analytics); window-averaged `temp_c` (representing the day's thermal environment); the current EWMA `gal_per_day` (a running smoothed consumption estimate); and the derived `days_until_empty` (current fill gallons divided by the EWMA rate). Batched and synced in a single cellular session. 365 Notes per tank per year.
-- `tank_alert.qo` — emitted on threshold trip with `sync:true`, bypassing the outbound timer. Suppressed for `alert_cooldown_hr` after each firing to prevent alarm fatigue.
-
-**Routed.** Notehub fans `tank_alert.qo` to whatever real-time channel the dealer's operations team uses (dispatch system webhook, SMS gateway, paging service) and `tank_status.qo` to a time-series store that feeds historical trend analysis and route-density modeling.
-
-**Alerts trigger on.**
-- `low_fill` — fill percentage drops below `fill_alert_pct` (default 20 %). Fires once per `alert_cooldown_hr` until the tank is refilled.
-- `high_consumption` — smoothed daily consumption rate has exceeded `consumption_alert_gal_per_day` (default 100 gal/day) for `consumption_alert_streak` consecutive sample cycles (default 3). The streak debounce prevents false positives from transient ADC jitter or short-lived post-refill consumption spikes; the counter resets to zero any time the rate drops back below threshold or the transmitter reports a fault. At the 15-minute default sample rate, a streak of 3 means the sustained high-rate condition must persist for at least 30 minutes before the alert fires. Most residential tanks run 3–15 gal/day in normal operation; 100 gal/day sustained flags a potential leak or piping fault.
-- `sensor_fault` — transmitter current outside the valid 4–20 mA window. Indicates wiring damage, a flooded enclosure, or a failed transmitter. Requires physical inspection.
-
-**Cloud-side usage model (high level).** The dealer's analytics system receives daily `tank_status.qo` Notes with `fill_gal` and `gal_per_day` for every tank. A simple projection — `days_until_empty = fill_gal / gal_per_day` — is computed by the firmware and included in each Note, so the dealer's route planner can sort tanks by urgency without running any analytics locally. Route density optimization (grouping tanks within a delivery window by geography and urgency) is a cloud-side concern, not a firmware concern; the device's job is to get accurate fill data into Notehub on time.
-
-## 9. Validation and Testing
-
-**Expected cadence in steady state.** A correctly-functioning unit on a residential propane tank should produce one `tank_status.qo` event per day and zero `tank_alert.qo` events during normal operation. During commissioning, expect a `sensor_fault` event if the transmitter wiring is incomplete, and low-fill alerts if the tank happens to already be below threshold.
-
-**Bench validation before field deployment.** To simulate the transmitter on the bench, drive the A0 analog input with a variable voltage in the 0.48–2.40 V range (a potentiometer from the 3.3 V rail to GND, or a bench DC supply, works). Confirm the firmware maps 0.48 V to 0 % fill (4 mA), 1.44 V to 50 % fill (12 mA), and 2.40 V to 100 % fill (20 mA). Verify the open-circuit condition (drive A0 to 0 V or below 0.42 V, corresponding to < 3.5 mA) emits a `sensor_fault` alert Note. For the DS18B20, confirm the firmware reports a valid temperature on the serial port and that a missing probe produces `NAN` (which becomes the `-9999` sentinel in summary Notes) rather than a garbage reading.
-
-**Commissioning calibration.** After mounting on the tank, set `tank_capacity_gal` to the tank's rated usable capacity in the fleet's Notehub environment variables. Verify that the fill percentage shown in Notehub matches the independent reference reading (mechanical gauge, weight, or LP technician's measurement) within a few percent. If there is a persistent offset that grows toward the high-fill end, the transmitter's span may not reach exactly 20 mA at the full-float position — adjust `sensor_full_ma` to the transmitter's actual full-scale output current until the Notehub reading aligns with the reference. If the offset is consistent across the full range (roughly constant error from empty to full), the transmitter may have a non-standard live-zero — adjust `sensor_empty_ma` from its default 4.0 mA to match the transmitter's actual output at the empty (float-at-bottom) position. Because the fill calculation is a simple linear interpolation with no temperature correction, the commissioning measurement is valid at any ambient temperature — no special thermal condition is required.
-
-**Using Mojo to validate power behavior.** Place the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the 5 V step-down output and the Notecarrier VBAT pad. The table below lists expected current envelopes on the 5 V rail for each major firmware state, with the source of each figure clearly identified.
-
-| Firmware state | Expected current (5 V rail, Mojo) | Source |
-|---|---|---|
-| Deep sleep (host cut via `card.attn`, Notecard idle, radio off) | ~8 µA @ 5 V | Blues-published idle figure for the NOTE-NBGLWX; see the [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) and [firmware best practices guide](https://dev.blues.io/connected-product-guidebook/firmware-best-practices-guide/). The Mojo measures the **entire powered subsystem** (Notecard + Notecarrier CX regulators), so the bench reading will be modestly above the Notecard-only figure; use trace shape and per-session energy as the primary commissioning targets, not an exact match to the Notecard-only idle number. |
-| Host awake — sampling (Cygnet active, 16-sample ADC average, DS18B20 750 ms conversion, I²C Notecard call, ~5 s total) | 30–50 mA | Bench estimate for the host subsystem only — not a Blues-published figure. Covers Cygnet STM32 active-mode current, 12-bit ADC operation, DS18B20 conversion current, and I²C Notecard transactions. The exact value varies with supply voltage and MCU clock. Trace shape (brief spike every `sample_interval_min`, then flat near zero) is the more reliable commissioning indicator than the absolute mA reading. |
-| Notecard network session — cellular (or satellite), small queued Note, good signal | in-session average ~250 mA from the onboard modem; brief peaks up to ~2 A for a few ms | Notecard for Skylo's Quectel BG95-S5 modem draws on the order of **~250 mA average** during a network session (cellular or satellite are similar in magnitude), with brief higher peaks — a 2G transmit burst can momentarily pull nearly **2 A for a few milliseconds** (see the [Blues low-power hardware design application Note](https://dev.blues.io/datasheets/application-notes/low-power-hardware-design/)). The Pololu D24V22F5 step-down (2.5 A rated) and SLA battery in this design comfortably source ≥ 2 A. Session duration is typically 30–60 s on cellular — expect a broad hump on the Mojo trace, not a sharp spike. Weaker signal or first-time network registration lengthens the session and raises per-session energy; the Blues [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) RSRP/SINR signal-quality table shows how signal conditions affect mAh per session. |
-
-The expected Mojo trace for a 15-minute sample / 24-hour cellular sync cycle: a brief (~5 s) 30–50 mA spike every 15 minutes from host-awake sampling, and once per day a broader 30–60 s hump averaging ~250 mA for the cellular sync. Between sample spikes the trace should sit flat at the ~8 µA idle floor — continuously elevated current between spikes means the host is not entering deep sleep (check the `card.attn` wiring and the `NotePayloadSaveAndSleep` return path). At the 15-minute default cadence the expected 24-hour Mojo total has three contributors: **(1) idle** — ~8 µA Notecard floor × 24 h ≈ **~0.2 mAh** (the total 5 V rail idle including Notecarrier CX regulator quiescent will read modestly above the Notecard-only figure at the bench); **(2) sampling** — 96 host-awake wakes/day × ~5 s @ 30–50 mA = **~4–6.7 mAh** (96 wakes × 5 s ÷ 3 600 s/h × 30–50 mA); **(3) daily sync** — ~250 mA × ~45 s ÷ 3 600 s/h ≈ **~3 mAh**. The expected daily total is therefore roughly **7–10 mAh/day**. If the Mojo 24-hour total runs materially above this range, the network session may be staying open longer than expected or the Notecard may be in `continuous` mode rather than `periodic`; confirm with `hub.status`.
-
-Note: the 4-20 mA transmitter loop is powered from the 24 V boost converter circuit and does **not** appear on the Mojo trace (Mojo measures the 5 V Notecarrier rail only). For a complete energy audit of the transmitter loop, place a bench ammeter in series with the boost converter's Vin+ lead on the 12 V system rail. Expect a draw of approximately 50–110 mW (4 mA loop, empty tank) to 550 mW (20 mA loop, full tank) from the 12 V rail continuously, accounting for boost converter losses; the 4–20 mA loop current itself can be read directly in serial debug output as `xmtr_ma`.
-
-**When operating over satellite (NTN).** The session-duration and daily-energy figures above describe cellular sessions. When Notecard for Skylo falls back to the Skylo satellite link the idle draw is unchanged (~8 µA) and a satellite session draws a similar ~250 mA average from the same modem, but session duration, the number of sessions Skylo's duty-cycle rules allow per day, and per-session energy differ from cellular. `sync:true` alert delivery depends on a satellite pass being available — latency is minutes, not seconds. Each inbound sync also consumes some of the bundled 10 KB satellite allocation, so widen `report_interval_hr` at satellite sites to conserve it. **Validate solar panel sizing, battery capacity, and alert latency separately for any site expected to operate over satellite** using the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) before commissioning; do not assume the cellular sizing figures in this section are sufficient.
-
-## 10. Troubleshooting
-
-**No device appearing in Notehub after power-up.**
-- Confirm the Notecard MAIN u.FL antenna is connected to an external antenna (not left stubbed internally). A missing or unplugged antenna will prevent cellular registration entirely.
-- Check that `PRODUCT_UID` is defined in the firmware and matches the UUID shown in Notehub under **Projects → ProjectUID**.
-- Verify that the 5 V step-down is outputting exactly 5.0 V (measured at Notecarrier +VBAT pad before applying load).
-- Check the serial console for Notecard and I²C errors. If unavailable, move the device to a location with strong cellular signal (≥2 bars) and reboot.
-
-**Device appears in Notehub but no events are arriving.**
-- Confirm the device is showing a recent session timestamp in Notehub (**Devices → [Your device] → Events → Device Activity**). A stale timestamp means the device is not connecting.
-- Check the Notecard's hub.status: connect via USB to the Notecarrier CX's USB-C port and open the [Notecard Playground](https://dev.blues.io/terminal/), then issue `{"req":"hub.status"}`. The response should show `"mode": "periodic"` and `"outbound": 24` (hours). If it shows `"status": "error"`, the Notecard may not be properly associated with the project.
-- Verify that environment variables are being delivered. Issue `{"req":"env.list"}` in Notecard Playground. If the response is empty, the device has not yet received the fleet's environment variables — this can take up to 48 hours at the default 24-hour inbound sync cadence (see §6 step 3).
-
-**Fill percentage is incorrect or drifts over time.**
-- Confirm the tank capacity is set correctly in the environment variable `tank_capacity_gal`. An incorrect capacity will scale all fill percentages proportionally.
-- Verify the transmitter is wired correctly: the loop supply must come from the 24 V boost converter output (not the 12 V rail), and the shunt-to-GND junction must connect to A0. At an empty tank (float at bottom), measure A0 with a meter — it should read ~0.48 V. At a full tank, it should read ~2.40 V.
-- If the fill reading is consistently high or low across the full range, the transmitter may have a non-standard live-zero or span. See §9 "Commissioning calibration" for how to adjust `sensor_empty_ma` and `sensor_full_ma`.
-
-**Alerts are firing constantly, or false `sensor_fault` alerts.**
-- A `sensor_fault` alert indicates the transmitter current is outside the 3.5–21 mA window (open circuit or short). Check the loop wiring: confirm both conductors from the transmitter are connected and not damaged, and that the shunt resistor is solidly connected.
-- High-consumption alerts may be false positives after a refill event. The firmware requires `consumption_alert_streak` consecutive above-threshold cycles before alerting; increase `consumption_alert_streak` in environment variables to reduce sensitivity to post-refill transients.
-- Set `alert_cooldown_hr` to increase the minimum hours between repeated alerts of the same type.
-
-**Very high or very low current readings in serial debug.**
-- The firmware reads the transmitter current every sample cycle and prints it to the serial console (USB-C on the Notecarrier CX). At an empty tank you should see ~4.0 mA; at a full tank, ~20.0 mA. If readings are consistently outside this range, re-check the shunt wiring and the 24 V boost converter output voltage (should be exactly 24 V measured at the transmitter + terminal with no load attached).
-
-**Battery drains quickly / device stops after a few hours.**
-- Confirm the host is actually entering deep sleep. The firmware calls `NotePayloadSaveAndSleep` after each sample cycle; if this fails, the host will run continuously and drain the battery in hours. Check that `card.attn` is properly wired (see §5 Step 5) and that the ATTN signal is reaching the Cygnet host.
-- If using the Mojo for validation, confirm it is removed before field deployment. The Mojo adds a continuous measurement load even during deep sleep.
-- For sites operating over the Skylo satellite (NTN) link, review the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) — satellite session timing and duty-cycle behavior differ from cellular and may shift the energy budget; size the battery and solar panel against the expected satellite session pattern.
-
-**WiFi is not connecting even after credentials are set.**
-- WiFi credentials are delivered via environment variables and applied on the next inbound sync. At the default 24-hour report cadence, inbound syncs occur every 48 hours — **credentials may take up to 48 hours to reach the device**. Temporarily lower `report_interval_hr` to 1 hour to speed delivery, then restore the original value after confirming connection.
-- Alternatively, set WiFi credentials via USB at the bench (see §6 step 3) before sealing the enclosure.
-- Notecard for Skylo's WiFi uses its onboard 2.4 GHz antenna; a metal enclosure will attenuate that signal. If WiFi fallback is important at this site, prefer a non-metallic enclosure or rely on cellular/satellite, which use the external MAIN antenna.
-
-## 11. Limitations and Next Steps
-
-This reference design covers the common case of a dealer instrumenting a tank fleet for demand-driven dispatch: a float transmitter at the gauge port, a temperature probe on the shell, cellular (or satellite) backhaul. The trade-offs below are deliberate scope choices; each has a clear extension path for a production rollout.
-
-### Simplified for the POC
-
-**Continuously-powered current loop.** The Rochester Sensors M6300-LP + R6315-12 and similar LP gauge-port float transmitters use a **continuously-powered 4–20 mA current loop**. Powered from the 24 V boost converter, the loop draws 96–480 mW from the 24 V rail (4 mA × 24 V to 20 mA × 24 V); accounting for the boost converter's ≈ 87 % efficiency, the draw on the 12 V system rail ranges from ≈ 110 mW (empty tank) to ≈ 550 mW (full tank) continuously. This is the dominant load in the system power budget. At worst case (full tank, 20 mA), the transmitter loop draws ≈ 550 mW for 24 h = ≈ 13.2 Wh/day from the 12 V rail; a 10 W solar panel at ≥ 4 h/day peak sun yields ≈ 40 Wh — well above the combined transmitter and electronics load for mid-latitude deployments. In low-sun environments or high-latitude winter deployments, size the panel to 20 W. Note: the tank spends most of its time between 20–80 % fill (8–16 mA), so the average draw is typically 250–370 mW, materially below the 550 mW worst case.
-
-**Fill-gallons calculation uses a linear scale.** `computeFillGal` computes `fill_gal = (fill_pct / 100) × tank_capacity_gal`. The horizontal-cylinder cross-section introduces a small nonlinearity between liquid height and volume (the relationship curves near the top and bottom of the tank). This nonlinearity is not corrected in firmware — an additional geometry correction table keyed to `tank_inner_diameter_in` could improve accuracy at fill levels below ~20 % or above ~80 %, at the cost of an additional environment variable and more complex calibration. For the typical operating range (20–80 % fill) the linear-scale error is small.
-
-**Satellite (NTN) operation requires an unobstructed sky-view antenna and an initial non-NTN sync.** Notecard for Skylo adds satellite with no extra module, but to use the Skylo link the Skylo-certified MAIN antenna must be mounted outdoors with a clear view of the sky (northern hemisphere: the southern sky); it cannot reach the satellite from beneath a metal obstruction. Satellite operation is also opt-in at the Notecard level: the firmware enables it by setting `card.transport` to `wifi-cell-ntn`, and the board must complete at least one initial non-NTN (cellular or WiFi) sync to associate with Notehub and register Notefile templates before NTN works — commission each unit where it has terrestrial coverage. Over satellite, expect alert latency in minutes (not seconds), keep each Note within the NTN payload budget, and remember that inbound syncs draw on the bundled 10 KB satellite allocation. At remote tank sites, confirm a suitable sky-view antenna location before relying on the satellite path.
-
-**Electronics must be outside the classified area; IS barrier may be required.** Blues Notecard and Notecarrier CX electronics are not rated for hazardous locations and must be installed outside the classified area boundary defined by NFPA 58 and the authority having jurisdiction. For 4–20 mA wiring that crosses the classified-area boundary, the AHJ may require a listed intrinsic-safety (IS) barrier in the loop circuit between the tank-side transmitter and the enclosure-side electronics. Transmitter selection, fitting compatibility, classified-area boundary determination, and any IS barrier requirement are outside the scope of the electronics design. Any connection to a propane pressure vessel must comply with NFPA 58, applicable codes, and be performed by a licensed LP gas technician.
-
-**Consumption rate requires stable readings over time.** The EWMA consumption rate is meaningful only after several days of operation. On first deployment, `days_until_empty` may be unreliable until the EWMA has converged. The firmware initializes consumption to zero and does not report `days_until_empty` until at least two consecutive valid fill readings exist.
-
-**No flow meter.** Consumption rate is estimated from successive fill readings, a delta-volume over delta-time approximation. This is accurate enough for daily route planning but not for billing or leak detection at high precision. A turbine or ultrasonic flow meter at the regulator outlet would give direct consumption measurement; integration would require an additional pulse-counting or analog input channel.
-
-**Mojo is bench-validation equipment only.** The firmware does not read the Mojo's LTC2959 coulomb counter at runtime. Adding cumulative mAh to the daily summary is a straightforward extension if fleet-level power telemetry is useful for the dealer's operations team.
-
-### Production Next Steps
-
-- Transmitter calibration tool: a Notehub JSONata route transform that reads `transmitter_ma` from the first three Notes after provisioning and auto-suggests a `sensor_full_ma` correction, removing the manual commissioning step.
-- Refill event detection: when `fill_pct` increases by more than 10 % in a single sample cycle, emit a `refill_detected` Note with the pre- and post-fill readings. This lets the dealer's billing system close the loop on deliveries without a separate ticket.
-- Per-tank consumption model: extend the EWMA with seasonal coefficients (summer vs. winter heating demand) so `days_until_empty` is corrected for known demand patterns, not just trailing-average consumption.
-- Over-the-air host firmware updates via [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) once the fleet reaches scale. A threshold recalibration or a new alert type can be pushed fleet-wide without a site visit.
-
-## 12. Summary
-
-The dealer who used to roll a truck every six weeks because that's what the calendar said now rolls a truck because a specific tank is at 22% and trending toward empty in nine days. A float transmitter at the gauge port, a temperature probe on the shell, a single Notecard for Skylo that carries cellular, WiFi, and satellite on one module — reaching cellular near town and the Skylo satellite network at the remote cabin with nothing to swap — and a daily summary in Notehub turn every tank in the territory into a self-reporting asset: fewer miles, fewer emergency calls, and no more Friday-night apologies to the customer whose heat just went out.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/propane-lpg-tank-fill-telemetry/)**
diff --git a/57-propane-lpg-tank-fill-telemetry/banner.png b/57-propane-lpg-tank-fill-telemetry/banner.png
deleted file mode 100644
index add3c8a8..00000000
Binary files a/57-propane-lpg-tank-fill-telemetry/banner.png and /dev/null differ
diff --git a/57-propane-lpg-tank-fill-telemetry/diagrams/01-system-architecture.svg b/57-propane-lpg-tank-fill-telemetry/diagrams/01-system-architecture.svg
deleted file mode 100644
index 70378d1d..00000000
--- a/57-propane-lpg-tank-fill-telemetry/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,66 +0,0 @@
-
diff --git a/57-propane-lpg-tank-fill-telemetry/diagrams/02-wiring-assembly.svg b/57-propane-lpg-tank-fill-telemetry/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 969ba89d..00000000
--- a/57-propane-lpg-tank-fill-telemetry/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,104 +0,0 @@
-
diff --git a/57-propane-lpg-tank-fill-telemetry/diagrams/03-data-flow.svg b/57-propane-lpg-tank-fill-telemetry/diagrams/03-data-flow.svg
deleted file mode 100644
index 628c5962..00000000
--- a/57-propane-lpg-tank-fill-telemetry/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,67 +0,0 @@
-
diff --git a/58-commercial-grease-interceptor-level-monitor/README.md b/58-commercial-grease-interceptor-level-monitor/README.md
index b881d367..a7a9dbe5 100644
--- a/58-commercial-grease-interceptor-level-monitor/README.md
+++ b/58-commercial-grease-interceptor-level-monitor/README.md
@@ -1,406 +1,5 @@
-# Hydromechanical (HGI) and Batch-Collection Grease Interceptor Level Monitor
+# Commercial Grease Interceptor Level Monitor
-
+A cellular device for pumping providers that reports commercial grease interceptor fill level every 15 minutes and dispatches alerts when a threshold is reached.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Hydromechanical%20%28HGI%29%20and%20Batch-Collection%20Grease%20Interceptor%20Level%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/grease_interceptor_monitor/](firmware/grease_interceptor_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [truck-roll reduction](https://blues.com/truck-roll-reduction/) device for pumping providers who service **commercial grease interceptors**. A waterproof ultrasonic distance sensor installed in the access cover reports fill level over cellular every 15 minutes; when the level reaches a threshold, an alert dispatches immediately to the [Blues Notehub](https://blues.com/notehub/) cloud service. Trucks are routed on actual condition, not a fixed calendar.
-
-**What you'll have at the end:** A weatherproof wall-mounted enclosure with a Notecarrier CX + Notecard Cell+WiFi and an inline Blues Mojo coulomb counter that samples a DFRobot ultrasonic sensor every 15 minutes, publishes daily summaries (including per-window energy consumption) and threshold alerts to Notehub, and lets you tune alert thresholds and sample intervals via fleet-level environment variables without re-flashing.
-
-**This reference implementation is scoped to hydromechanical (HGI) and batch-collection interceptors without a fixed outlet weir** — geometries where the liquid surface rises predictably with FOG accumulation, making fill percentage a direct proxy for pump-out urgency. See §1 and §10 before deploying on a conventional constant-level gravity interceptor where a fixed weir holds the surface height independent of FOG-layer depth.
-
-## 1. Project Overview
-
-**The problem.** Every commercial kitchen is legally required to install a **grease interceptor** (sometimes called a grease trap) — a chamber plumbed into the kitchen drain line that intercepts **FOG** (fats, oils, and grease) before it enters the municipal sewer. FOG accumulates as a floating layer at the top of the interceptor. Left unchecked, that layer eventually reaches the inlet pipe and starts flowing into the sewer, a violation that can result in fines, shutdowns, and backups into the kitchen floor drains.
-
-To stay compliant, **FSEs** (food service establishments, restaurants, cafeterias, ghost kitchens, institutional food service) hire a pumping provider who periodically vacuums the trap clean. The industry default is a fixed service cadence: the truck shows up every four weeks, or every two weeks, regardless of how full the trap actually is. In practice, a slow-season week produces far less grease than a holiday rush. Fixed cadence means the pumper sometimes arrives when the interceptor is only 30% full — a truck roll that did nothing for compliance, and sometimes arrives after the interceptor has already overflowed. Neither outcome is good, and the FSE pays for every dispatch.
-
-A fill-level sensor changes the economics entirely. With a real-time reading, the pumping provider dispatches trucks on condition rather than on schedule. Interceptors that fill quickly (high-volume kitchens, fryer-heavy menus) get serviced before they overflow; interceptors that fill slowly get serviced less often without the compliance risk. The result is fewer unnecessary truck rolls, better route efficiency, and a condition-based service story the pumper can offer as a premium tier. **This reference design targets hydromechanical (HGI) and batch-collection interceptors without a fixed outlet weir — geometries where the top liquid surface rises predictably as FOG and wastewater accumulate, so a rising fill percentage is a meaningful proxy for pump-out urgency.** For deployments on conventional constant-level gravity interceptors, where an outlet weir holds the liquid surface nearly fixed regardless of FOG-layer depth, see the measurement-model Note below and §10.
-
-
-
-**Measurement model — read before deployment.** The firmware measures the **distance from the probe face to the top of the liquid/FOG surface** and converts it to a "fill percentage" of the working liquid column. In a **conventional constant-level grease interceptor** — where the outlet weir elevation fixes the working liquid height — the top liquid surface stays nearly constant as FOG accumulates beneath it, so this single top-down distance reading **does not track FOG-layer thickness** and **does not map to regulatory pump-out criteria**. This reference design produces the most actionable readings in geometries where the top liquid surface does rise with accumulation (e.g., hydromechanical or batch-collection interceptors without a fixed weir). On a conventional constant-level interceptor, treat `fill_pct` as a liquid-level proximity proxy only — **not as a compliance metric**, and validate empirically before relying on the 75% alert threshold for dispatch decisions.
-
-
-
-**Why Notecard.** Grease interceptors are typically located outside the building — in the rear yard, buried in a rear parking lot, or set into the floor of a utility room in the basement. None of these locations have reliable access to the restaurant's WiFi. And unlike a single building-owner deploying one device, a pumping provider is managing hundreds of independent FSE accounts spread across a city, each with a different WiFi network, a different IT contact (usually "the owner's nephew"), and a different level of willingness to hand out network credentials to a pumping vendor. Cellular removes all of that friction: no access point to pair to, no password to manage, no IT ticket to chase. The Notecard Cell+WiFi ships with a prepaid global SIM, so the same hardware-firmware combination deploys identically at every FSE on a pumper's route — from a two-seat taqueria to a stadium commissary. WiFi remains as an opportunistic fallback for the rare installation in a utility room that happens to have building WiFi overhead, without any firmware changes.
-
-
-
-**Deployment scenario.** A small weatherproof electronics box mounted on the wall adjacent to the interceptor access cover. **This documented build targets indoor utility-room HGI installations where a standard 120 VAC wall outlet is accessible within ~1.5 m of the installation point.** A DFRobot A02YYUW IP67-rated ultrasonic probe is mounted through a 22 mm clearance hole drilled in the interceptor's access cover (one cover penetration per installation), held by a P-clip and sealed with an IP67 silicone grommet and RTV. See §5 for the full mechanical procedure. The probe hangs below the cover, pointing at the liquid/FOG surface. The box is powered by an external UL-listed 5 V DC wall adapter; a low-voltage DC pigtail cable enters the sealed enclosure through a cable gland and connects directly to the Notecarrier CX `+VBAT` header pin — no mains wiring inside the enclosure, and VUSB stays absent so the Notecard reaches its lowest idle power floor (~8 µA between syncs). No drilling into the interceptor tank body or drain piping, no drain-line modification, and no coordination with the building owner beyond "we're mounting a small sensor on your grease trap cover and a box on the wall next to it." For outdoor, rear-yard, or below-grade installations without accessible indoor power, see [Limitations](#10-limitations-and-next-steps).
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Inside the wall-mounted enclosure, the Cygnet STM32L433 host on the Notecarrier CX wakes every 15 minutes (configurable), reads the ultrasonic probe over hardware UART, converts the median distance to a fill percentage, and checks the alert threshold. Once per daily summary window it also reads the Mojo coulomb counter via [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power) to capture the energy consumed that window. When it's done, it hands any resulting Note to the Notecard over I²C and uses [`NotePayloadSaveAndSleep`](https://dev.blues.io/guides-and-tutorials/notecard-guides/feather-mcu-low-power-management/) to serialize its runtime state — running fill total, peak fill, last alert and report timestamps — into Notecard flash and cut its own power. The next wake comes up fresh, rehydrates the state, and picks up where it left off; no JSON marshaling, no AT commands ever touched the wire.
-
-**Notecard responsibilities.** The Notecard holds Notes in its local queue and runs two independent schedules configured by [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set): an **outbound** cadence (default every 24 hours) for flushing queued [Notes](https://dev.blues.io/api-reference/glossary/#note) to Notehub, and a fixed **inbound** cadence (every 2 hours, set by `HUB_INBOUND_MIN`) for polling Notehub for updated environment variables. Anything tagged `sync:true` skips both windows and opens an immediate session. The same [environment variable](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) mechanism is how a pumping provider retunes interceptor depth, alert threshold, and sampling cadence across an entire route from a browser — no reflash, no site visit.
-
-**Notehub responsibilities.** The Notecard's embedded global SIM lands events in [Notehub](https://notehub.io), which ingests them, stores every one, and runs the project's routes. Daily summaries and threshold alerts live in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) so each can flow to a different downstream — `grease_alert.qo` to the dispatch endpoint that pages a truck, `grease_summary.qo` to the analytics store that builds the per-FSE fill trends.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-### First Event in Minutes
-
-1. **Flash the firmware** (§7) with your ProductUID.
-2. **Set DIP switch to HST** and connect USB-C to a laptop. Open the serial monitor at 115200 baud.
-3. **Take a tape measurement** from the sensor face to the liquid surface.
-4. **Override `sample_interval_sec`** to `60` via Notehub fleet environment variables (Fleet → Environment) so you get a reading every minute instead of every 15.
-5. **Watch the serial output** for `[DBG] median distance mm: XXX` on each wake. Compare against your tape measurement — should match within 1–2 cm.
-6. **Check Notehub events:** Within a few minutes you should see a `grease_summary.qo` arrive (or `grease_alert.qo` if you also lowered `alert_threshold_pct` to 5 to test the alert path).
-7. **Restore `sample_interval_sec` to 900** before leaving the site.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "grease_summary.qo",
- "body": {
- "fill_pct_avg": 42.3,
- "fill_pct_peak": 51.7,
- "fill_pct_now": 44.1,
- "valid_samples": 94,
- "power_mah": 248.6
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an onboard Cygnet STM32L433 host MCU — no separate host board needed. ATTN pin wiring to control the host power rail is built in. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([Datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular-first connectivity removes per-site WiFi dependency; WiFi fallback available for installations in utility rooms with accessible APs. Ships with a prepaid global SIM. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | **Required, permanently installed.** LTC2959 coulomb counter wired inline on the +VBAT rail and connected to the Notecarrier CX Qwiic port. Notecard firmware (v8.1.3+) auto-detects it over I²C; the firmware reads per-window energy via [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power) and reports it as `power_mah` in every daily summary (see §9). |
-| [Qwiic cable](https://www.sparkfun.com/qwiic) (4-pin JST-SH, 50–100 mm) | 1 | Connects the Mojo to the Notecarrier CX Qwiic port so the Notecard can read the coulomb counter over I²C. A short cable ships with most Mojo units; confirm before ordering. |
-| [DFRobot A02YYUW Waterproof Ultrasonic Sensor (SEN0311)](https://www.dfrobot.com/product-1935.html) | 1 | IP67-rated ultrasonic probe with UART output; range 3–450 cm; average current ≤8 mA. The IP67 rating covers ingress protection against dust and temporary water immersion — it establishes resistance to splash and condensation but does **not** address long-term chemical compatibility with grease, H₂S, sewer gas condensate, or cleaning chemicals found in interceptor atmospheres. Chemical durability in that environment is unverified for this sensor; field-validate before committing to a deployment, or substitute a sensor with explicit chemical-resistance ratings for the target environment. |
-| [Hammond 1554W2GY](https://www.hammfg.com/part/1554W2GY) enclosure, 180 × 180 × 66 mm, IP68 polycarbonate | 1 | Sized for the Notecarrier CX (≈80 × 56 mm footprint) with room for the Mojo and cable management. |
-| [SparkFun CEL-16432 LTE Hinged External Antenna](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html), SMA male, 698 MHz–2.7 GHz | 1 | Hinged external antenna mounted on the polycarbonate enclosure via an SMA bulkhead — covers all MBGLW LTE Cat-1 bis bands across the 698 MHz–2.7 GHz range. |
-| UL-listed 5 V / 2 A (min) regulated switching wall adapter with DC pigtail output, ≥ 1.5 m cable (any UL/CSA-listed 5 V, 2 A or higher regulated switching wall adapter with bare-wire or 2.1 mm center-positive barrel-jack output) | 1 | All mains wiring stays external to the enclosure — only low-voltage DC enters through the cable gland. The positive lead connects to the Notecarrier CX `+VBAT` pad and the negative lead to `GND`, keeping VUSB absent so the Notecard reaches its lowest idle current (~8 µA between cellular syncs). The 2 A minimum rating ensures the supply can sustain the MBGLW's ≤2 A peak transmit bursts without browning out. If your adapter has a 2.1 mm barrel-jack output, use a short 2.1 mm pigtail-to-stripped-lead adapter cable inside the enclosure. **Do not use a USB charger:** USB power appears at the CX USB-C port (VUSB present), which prevents the Notecard from reaching its lowest idle power floor. |
-| M16 cable gland, IP68-rated | 2 | One in the enclosure wall for the sensor cable (seals the four bare sensor wires after the PH2.0 connector is cut off; see Wiring). One in the enclosure wall for the DC power pigtail entry. M16 glands seal cables up to approximately 10 mm OD — confirm your adapter cable jacket fits before drilling. The interceptor cover penetration uses a wider opening and a bracket/grommet mount. See the BOM rows below and the Wiring section. |
-| Silicone boot grommet, 22 mm OD / 19 mm ID (or nearest available; must grip a ~19 mm cylindrical probe body), IP67-rated | 1 | Seals the 22 mm cover penetration around the A02YYUW probe body. **The probe head is ~19 mm in diameter and cannot pass through an M16 cable gland bore — a wider opening and grommet are required.** |
-| Two-piece P-clip or saddle clamp, 19 mm bore, stainless or nylon | 1 | Secures the probe body from above the interceptor access cover after insertion, preventing the probe from sliding into the chamber and maintaining the correct probe-face depth below the cover. |
-| Neutral-cure RTV silicone sealant (food-safe / H₂S-resistant, e.g., Permatex #80050 or equivalent) | 1 small tube | Seals around the probe body at the cover penetration and around the gland bodies. Apply after final probe positioning; allow to fully cure before replacing the cover. |
-| Female DuPont crimp contacts, 2.54 mm pitch, 4-position housing with crimp pins (or 4 × female-to-female 2.54 mm jumper wires, one end cut and stripped) | 1 set | Terminates the four stripped sensor leads for insertion onto the Notecarrier CX's 2.54 mm header pins. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-⚠️ **Safety — wastewater and sewer gas.** Grease interceptors accumulate hydrogen sulfide (H₂S) and other sewer gases that are toxic at low concentrations and flammable at higher ones. Before opening any access cover: ventilate the area, test with a gas monitor if available, keep ignition sources clear, and wear appropriate PPE (nitrile gloves, eye protection). Do not enter a below-grade vault — confined-space entry requires dedicated procedures, a gas monitor, a standby person, and, in many jurisdictions, a permit. This reference design mounts the probe from above with the cover ajar and never requires anyone to enter the interceptor chamber.
-
-⚠️ **Hazardous-location classification — confirm before deployment.** The A02YYUW probe and all wiring and electronics in this build are standard, **non-intrinsically-safe (non-IS)** components with no NEC Article 505/506 (Class/Zone), ATEX, or IECEx certification. Suspending energized, non-IS electronics inside an interceptor atmosphere bearing H₂S or flammable sewer gases may be prohibited by local electrical code (NEC, NFPA 820, or equivalent). The determination of whether a given installation constitutes a classified hazardous location must be made by a qualified licensed electrician or the local authority having jurisdiction (AHJ) on a site-by-site basis — do not assume an installation is unclassified without a documented review. **This reference design is suitable only for non-classified installations, and only after a local code and site-safety review is complete and documented.** Classified hazardous-location installations require intrinsically safe (IS) or explosion-proof (XP) sensors, certified IS barriers, appropriately rated enclosures, and installation by personnel qualified for hazardous-area work.
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi seats into the CX's M.2 slot; the SparkFun CEL-16432 hinged LTE antenna mounts externally on the polycarbonate enclosure via an SMA bulkhead connector. **The Blues Mojo coulomb counter is a permanent part of the deployed unit.** It is wired inline on the +VBAT rail (so it integrates the whole-board current) and connected to the Notecarrier CX Qwiic port (so the Notecard can read it over I²C). The Notecard auto-detects the Mojo and the firmware reports per-window energy consumption (`power_mah`) in every daily summary via [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power) — see §9 for the wiring detail and validation procedure.
-
-The A02YYUW sensor has a 2-meter cable terminating in a 4-pin PH2.0 connector: **VCC** (red), **GND** (black), **RX** (yellow, mode-select), and **TX** (white, UART data out). **Cut off the PH2.0 connector** — the molded housing is wider than the M16 enclosure gland's bore and will not pass through. Strip 8 mm from each wire, feed the four bare ends through the enclosure's M16 cable gland from outside, tighten the compression nut on the cable jacket, then fit a female 2.54 mm DuPont crimp contact onto each stripped lead and seat them in a 4-pin housing (or use individual housings). This terminated end lands on the Notecarrier CX's 2.54 mm header as described below.
-
-Pin-by-pin:
-
-- **+3V3\_OUT** → A02YYUW **VCC** (red). `+3V3_OUT` is the exact label on the Notecarrier CX dual 16-pin header; it supplies 3.3 V at up to 100 mA. The sensor draws ≤8 mA average, well within that limit.
-- **GND** → A02YYUW **GND** (black) and A02YYUW **RX** (yellow). Tying the sensor's RX pin to GND selects real-time continuous output mode — the sensor streams packets as fast as it measures. Leave the RX pin floating as an alternative (the internal pull-down produces the same behavior).
-- **RX** header pin → A02YYUW **TX** (white). `RX` is the exact label on the Notecarrier CX header for the Cygnet's hardware UART receive line. The firmware assigns `Serial1` to this UART instance when built for the Notecarrier CX's onboard Cygnet — this mapping is correct for the standard target. If you retarget the sketch to a different STM32 board or core variant, confirm that `Serial1` maps to the `RX`/`TX` header pins for that board before wiring.
-- **+VBAT** pad → the Mojo's output, with the Mojo spliced inline on the positive rail: DC wall adapter positive lead (red or marked +5V) → Mojo input; Mojo output → CX `+VBAT` pad. The adapter negative lead connects to `GND`. This routes all board current through the Mojo so it measures whole-board draw. **Do not connect the CX's USB-C port to any power source during normal operation** — VUSB must remain absent for the Notecard to reach its lowest idle power floor (~8 µA). See §9 for the exact Mojo terminal labels.
-- **Qwiic port** → Mojo Qwiic connector. A 4-pin JST-SH Qwiic cable from the Notecarrier CX Qwiic port to the Mojo lets the Notecard read the coulomb counter over I²C. No other configuration is needed — the Notecard auto-detects the Mojo.
-
-**Power entry.** The DC wall adapter plugs into a standard wall outlet outside the enclosure; only the low-voltage DC pigtail enters the box. Drill an M16 clearance hole in the enclosure wall for the DC cable; fit the M16 gland body from outside and secure with its locknut from inside. Thread the DC pigtail through the gland from outside and tighten the compression nut on the cable jacket to grip and strain-relieve it. Inside the enclosure, trim the leads to length, strip 8 mm of insulation from each, and connect the positive lead to the CX `+VBAT` pad and the negative lead to `GND` (use female 2.54 mm DuPont crimp contacts or solder directly to the header pads). If your adapter has a 2.1 mm barrel-jack output, use a short barrel-to-DuPont pigtail adapter and connect as above. There is no mains voltage inside the enclosure; no insulation clearance, creepage, or earth-bonding requirements apply to the DC wiring.
-
-**Sensor cable entry and probe mounting.** ⚠️ The A02YYUW transducer head is approximately 19 mm in diameter — it **will not pass through an M16 cable gland bore** (typically 13 mm maximum insert diameter). Do not attempt to mount the probe through an M16 gland. Use the following bracket-and-grommet approach:
-
-1. **Mark and drill.** Center-mark the interceptor access cover over the chamber opening. Drill a **22 mm clearance hole** — a 22 mm step drill or bi-metal hole saw works on cast-iron, fiberglass, and polymer covers. If the cover already has a plugged service port of 22 mm or larger, clean it out and reuse it. Deburr the hole edge.
-2. **Thread the cable through first.** Feed the bare (connector-stripped) sensor leads through the 22 mm hole from above. The cable jacket is small enough to pass through easily; the probe head remains above the cover at this stage.
-3. **Seat the grommet.** Press a 22 mm OD / 19 mm ID IP67-rated silicone boot grommet into the hole from above. The grommet should seat flush with the cover surface; its inner bore will grip the probe body with light friction when the probe is inserted.
-4. **Insert the probe.** Push the probe body down through the grommet from above, face pointing down, until the transducer face is at least 30 mm below the underside of the cover. (30 mm is the sensor's minimum blind zone, anything closer returns invalid data.) The cable now exits upward above the cover.
-5. **Secure with a P-clip.** Fit a two-piece 19 mm bore P-clip or saddle clamp around the probe body flush against the top of the cover. Tighten the clip so the probe cannot slide down into the chamber and the face-to-cover distance is fixed. Confirm the transducer face is still at least 30 mm below the cover underside after tightening.
-6. **Seal with RTV.** Apply a continuous bead of neutral-cure RTV silicone sealant around the probe body at the grommet–cover interface on both the upper and lower face. Allow to cure fully (typically 24 hours) before replacing the access cover and closing the hatch.
-
-Route the cable from the cover to the enclosure box on the adjacent wall and enter through the enclosure's M16 cable gland as described in the sensor cable preparation above.
-
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/grease_interceptor_monitor/grease_interceptor_monitor.ino` as `PRODUCT_UID`.
-2. **Claim the Notecard.** Power the unit; on first cellular connection the Notecard auto-associates with your project.
-3. **Create a Fleet per pumping route.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) are how Notehub groups devices for shared configuration and routing. A natural fit here is one fleet per service route — all interceptors on the same truck route likely share similar sizes and fill cadences, so fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) can encode route-wide defaults and you override on a per-device basis for unusual installations. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can be used to auto-assign devices based on FSE metadata tags.
-4. **Set environment variables.** All variables below are optional; firmware defaults are shown. Any value set in Notehub overrides the compile-time default on the device's next inbound sync — no re-flashing required.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `interceptor_depth_mm` | `600` | Distance (mm) from the sensor face to the liquid surface **immediately after pump-out** (0% grease fill). Measure once per installation during commissioning — it varies by interceptor model and probe mounting height. |
- | `alert_threshold_pct` | `75` | Fill percentage at or above which a `grease_alert.qo` Note fires. On HGI units without a fixed weir, the fill percentage reflects the combined liquid-plus-FOG working volume, so this threshold directly corresponds to pump-out urgency — calibrate empirically during commissioning by observing what fill level your target service interval produces. The default 75% is a reasonable starting point for most HGI geometries. See §10 if deploying on a conventional constant-level interceptor. |
- | `sample_interval_sec` | `900` | Seconds between distance readings (default 15 minutes). Grease accumulates over hours to days — 15 minutes is more than sufficient resolution. |
- | `report_interval_min` | `1440` | Minutes between summary Notes (default 24 hours). The firmware re-issues `hub.set` whenever this value changes, keeping the Notecard's **outbound** sync cadence aligned with the local summary period — lowering this to 360, for example, means summaries are emitted every 6 hours and the outbound batch window shrinks to match. The **inbound** cadence (environment-variable polling every 2 hours) is independent of this setting and does not change. |
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) targeting `grease_alert.qo` for real-time dispatch notification (webhook to a field-service platform, SMS gateway, or dispatch email), and a second for `grease_summary.qo` pointed at a long-term analytics store. Keeping the two Notefiles separate at the source means each can be routed to a different destination without filtering logic in the route itself.
-
-## 7. Firmware Design
-
-### Building and Flashing
-
-1. **Clone this repository** and open the sketch in the Arduino IDE or use `arduino-cli` from the command line.
-2. **Install dependencies:**
- ```bash
- arduino-cli lib install "Blues Wireless Notecard"
- arduino-cli core install STMicroelectronics:stm32
- ```
-3. **Configure the board:** Select **Tools > Board > STMicroelectronics STM32 > Notecarrier CX** (if using Arduino IDE), or pass the equivalent to `arduino-cli`.
-4. **Set your Product UID:** Edit `grease_interceptor_monitor.ino` line 43 and paste your Notehub `ProductUID`:
- ```cpp
- #define PRODUCT_UID "com.your-company.your-name:your_project"
- ```
-5. **Build and upload:**
- ```bash
- arduino-cli compile --board STMicroelectronics:stm32:Notecarrier_CX firmware/
- arduino-cli upload --port /dev/ttyUSB0 --board STMicroelectronics:stm32:Notecarrier_CX firmware/
- ```
- (Replace `/dev/ttyUSB0` with your serial port; use `arduino-cli board list` to find it.)
-6. **Monitor the serial output** at 115200 baud to confirm sensor readings and Notecard communication.
-
-Firmware files:
-
-| File | Purpose |
-|---|---|
-| [`grease_interceptor_monitor.ino`](firmware/grease_interceptor_monitor/grease_interceptor_monitor.ino) | Main sketch: `setup()` / `loop()`, Notecard configuration, env-var fetch, alert/summary scheduling, sleep |
-| [`grease_interceptor_monitor_helpers.h`](firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.h) | Shared constants (`SENSOR_BAUD`, `NUM_READINGS`, …), `State` struct definition, utility-function declarations |
-| [`grease_interceptor_monitor_helpers.cpp`](firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.cpp) | Utility-function implementations: sensor read, median filter, distance-to-fill, Notecard response helpers, Note emission |
-
-Dependencies:
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (`note-arduino`). Install via the Arduino Library Manager (`arduino-cli lib install "Blues Wireless Notecard"`) or download from [the releases page](https://github.com/blues/note-arduino/releases).
-
-### Modules
-
-| Responsibility | Function / symbol | File |
-|---|---|---|
-| Notecard configuration (`hub.set`, accelerometer disable) | `notecardConfigure` | `.ino` |
-| Note template definition | `defineTemplates` | `.ino` |
-| Environment variable fetch; re-applies `hub.set` when `report_interval_min` changes | `fetchEnvOverrides` | `.ino` |
-| UART sensor read with packet validation | `readDistanceMm` | `_helpers.cpp` |
-| Median filter over multiple readings | `medianOf` | `_helpers.cpp` |
-| Distance-to-fill-percent conversion | `distanceToFillPct` | `_helpers.cpp` |
-| Notecard response validation and error logging | `notecardResponseOk` | `_helpers.cpp` |
-| Threshold evaluation and alert emission | inline in `setup`, `sendAlert` | `.ino` / `_helpers.cpp` |
-| Daily summary accumulation and emission | inline in `setup`, `sendSummary` | `.ino` / `_helpers.cpp` |
-| Persistent state across sleep cycles | `State` struct + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` | `_helpers.h` / `.ino` |
-
-### Sensor reading strategy
-
-The A02YYUW streams 4-byte UART packets continuously at 9600 baud: `[0xFF][high][low][checksum]`. The distance in mm is `(high << 8) | low`; the checksum is `(0xFF + high + low) & 0xFF`. On each wake, the firmware flushes any bytes that accumulated during sleep, then synchronizes to the next valid start byte (`0xFF`) and reads a complete packet. To suppress acoustic multipath artifacts (reflections off the interceptor walls can occasionally return a short or long reading), the firmware takes five readings in sequence and returns the median. Any reading below 30 mm or above 4500 mm is rejected as outside the sensor's rated range. The firmware also applies an installation-specific upper bound: any reading above `interceptor_depth_mm × 1.1` is rejected, providing 10 % headroom above the commissioning reference to catch near-overflow conditions while discarding obviously erroneous long readings. **If `interceptor_depth_mm` is configured shorter than the true sensor-to-surface distance at pump-out, this gate will reject every sample, `valid_samples` will remain zero, and no summary Note will be produced**. See the commissioning-depth diagnostic in §9.
-
-### Event payload design
-
-One [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) daily summary Note (`grease_summary.qo`), plus an immediate alert (`grease_alert.qo`) whenever `fill_pct` is at or above the alert threshold and the 1-hour cooldown has elapsed — which means a new alert fires every cooldown interval for as long as the interceptor remains above threshold. The template fixes each summary to a compact binary record (approximately 32 bytes on the wire), a meaningful saving over a full deployment lifetime with a sensor that's been installed for years in hundreds of interceptors. See "Example payloads routed to Notehub" below (§8) for the payload shape.
-
-### Low-power strategy
-
-Even though a back-of-house HGI installation has mains power available, the firmware still puts the host to sleep between samples — less heat in the enclosure and a firmware pattern that ports to a solar- or battery-backed variant without rearchitecting. After each sample cycle the host calls `NotePayloadSaveAndSleep`, which serializes the runtime state into Notecard flash and uses [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to cut host power for `sample_interval_sec` seconds. The Notecard itself idles at roughly 8 µA between cellular wakes when powered via `+VBAT` with VUSB absent — which this build achieves by routing the 5 V DC pigtail from the wall adapter to the CX's `+VBAT` header pin rather than its USB-C port. The A02YYUW draws ≤8 mA from the `+3V3_OUT` pin; the firmware does not actively switch sensor power, and the Notecarrier CX datasheet does not document whether the `+3V3_OUT` rail is cut when `NotePayloadSaveAndSleep` puts the Cygnet to sleep. Treat the sensor current as a continuous ~8 mA load when sizing the power budget. A continuous 8 mA sensor draw accumulates roughly 192 mAh over a 24-hour day — far more than a single daily LTE Cat-1 bis sync (~250 mA average for ≤60 seconds ≈ 4 mAh). The cellular sync is the dominant current spike, but the always-on sensor is likely the dominant daily energy consumer unless the `+3V3_OUT` rail is switched off during host sleep (its behavior during sleep is not documented in the CX datasheet and has not been verified for this build). Sensor power switching — a GPIO-controlled load switch on the sensor supply rail — is therefore the primary optimization to pursue before moving to a battery-backed installation (see §10). Until that is confirmed empirically with Mojo, treat the sensor as always powered (see §9 Mojo validation).
-
-Sampling and summary cadence are deliberately decoupled: the sensor fires every `sample_interval_sec` (default 15 minutes, 96 times per day at default), but outbound summary syncs occur only once per `report_interval_min` (default 24 hours). Separately, the Notecard opens an **inbound** session every 2 hours (`HUB_INBOUND_MIN = 120`) to poll Notehub for updated environment variables — these inbound sessions require a full radio wake and session establishment and must be budgeted in any power analysis (see §9). At the default 2-hour inbound cadence, expect up to 12 inbound wakes per day in addition to the single daily outbound sync and any alert syncs. Alert Notes set `sync:true` and bypass both scheduled windows entirely, waking the radio immediately on each alert fire — which repeats every cooldown interval as long as fill remains at or above threshold.
-
-**Typical power draw at 5 V DC (bench, with 5 V wall adapter):** The dominant consumer is the always-on A02YYUW sensor at approximately 8 mA continuous (~192 mAh over 24 hours). The Notecard idles at ~8 µA between syncs. One daily outbound sync (LTE Cat-1 bis, ~30–60 seconds at ~250 mA average) adds roughly 4–8 mAh; 12 daily inbound polls (~15 seconds at similar current) add approximately 50 mAh. **Conservative daily budget: ~250 mAh.** On a UL-listed 2 A 5 V wall adapter (minimum recommended), the power supply can sustain the peak 2 A Notecard transmit bursts indefinitely and has margin for future expansion. Mojo coulomb-counter validation (§9) is recommended before any battery-backed variant to confirm the sensor rail behavior during host sleep.
-
-### Retry and error handling
-
-- The first Notecard transaction on cold boot uses `sendRequestWithRetry(req, 5)` to handle the known I²C race condition where the host powers up before the Notecard is ready.
-- Sensor reads that fail checksum, time out, or return out-of-range values (below 30 mm, above 4500 mm, or above `interceptor_depth_mm × 1.1`) return `-1.0` and are excluded from the rolling average and peak. The firmware requires at least two valid readings out of five before computing a fill percentage; if fewer than two are valid, the sample window is silently skipped — no Note is emitted for that window (the firmware still calls `env.get` and `card.time` on every wake regardless). No sentinel values are written — bad data is simply absent from that sample window.
-- The alert cooldown (`ALERT_COOLDOWN_SEC = 3600`) prevents a near-threshold interceptor from triggering a dispatch notification every 15 minutes. One alert per hour is more than sufficient to escalate a genuine overflow risk.
-- `env.get` is called with a `names` array on every wake so the response always reflects the current operator-configured values, regardless of which variable was most recently updated on the server.
-- Whenever `report_interval_min` changes, `fetchEnvOverrides` immediately re-issues `hub.set` with the new `outbound` value so the Notecard's cellular sync cadence stays aligned with the local summary period. Without this, summaries queued at the new (shorter) interval would sit in the Notecard's store until the old (longer) outbound window fired.
-
-### Note template format codes (abbreviated syntax)
-
-Notehub templates use numeric codes to denote field types. The two codes used by this firmware:
-- `TFLOAT32` (`14.1`) — 4-byte IEEE 754 single-precision float
-- `TUINT32` (`24`) — 4-byte unsigned integer
-
-The numeric code conveys both byte width and signedness, for example `14` is a 4-byte *signed* integer (TINT32) while `24` is a 4-byte *unsigned* integer (TUINT32). The named macros (`TFLOAT32`, `TUINT32`, etc.) are defined by the `note-c` library that ships with the Blues Wireless Notecard library, so the firmware uses the names and the wire-format numbers stay an internal detail. Full reference: [Notehub template field types](https://dev.blues.io/api-reference/notecard-api/note-requests/#note-template).
-
-### Key code snippet 1: Note template definition
-
-The template makes each summary a fixed-length record.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "grease_summary.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "fill_pct_avg", TFLOAT32);
-JAddNumberToObject(body, "fill_pct_peak", TFLOAT32);
-JAddNumberToObject(body, "fill_pct_now", TFLOAT32);
-JAddNumberToObject(body, "valid_samples", TUINT32);
-JAddNumberToObject(body, "power_mah", TFLOAT32); // mAh consumed this window (Mojo)
-notecard.sendRequest(req);
-```
-
-### Key code snippet 2: immediate-sync alert
-
-`sync:true` tells the Notecard to bypass the outbound batch window and open a cellular session as soon as the Note is enqueued, typically within 15–60 seconds of the threshold trip.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "grease_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "fill_threshold_exceeded");
-JAddNumberToObject(body, "fill_pct", fill_pct);
-JAddNumberToObject(body, "threshold_pct", threshold_pct);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 3: sleep and state persistence
-
-`NotePayloadSaveAndSleep` stores the runtime state in Notecard flash and then cuts host power via `card.attn`. On the next wake, `NotePayloadRetrieveAfterSleep` rehydrates the state and the firmware resumes exactly where it left off — fill accumulator, peak, timestamps and all.
-
-```cpp
-NotePayloadDesc new_payload = {0, 0, 0};
-NotePayloadAddSegment(&new_payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&new_payload, cfg.sample_interval_sec, NULL);
-```
-
-## 8. Data Flow
-
-
-
-Every 15 minutes the Cygnet host wakes, fires the sensor five times, takes the median distance, and converts it to a fill percentage. That reading is added to the rolling daily accumulator. Two conditional paths run in parallel:
-
-- **Threshold alert.** If `fill_pct >= alert_threshold_pct` and at least one hour has elapsed since the last alert, a `grease_alert.qo` Note with `sync:true` is queued. The Notecard immediately opens a cellular session and delivers the Note to Notehub, which routes it to the dispatch endpoint.
-- **Daily summary.** Once every `report_interval_min` minutes (default 24 hours), a `grease_summary.qo` Note is queued containing the average fill percentage over the period, the peak fill percentage, the most recent instantaneous reading, and the count of valid samples. The Notecard's outbound sync cadence is kept aligned with `report_interval_min` by the firmware, so the Note ships on the next outbound window — which matches the summary period.
-
-**Collected:** Distance from sensor face to top of liquid/FOG surface (mm); fill percentage derived from that distance (on HGI units without a fixed weir, this tracks combined liquid-plus-FOG accumulation. See §10 for behavior on other interceptor geometries); timestamp (from Notecard's synced clock).
-
-**Transmitted:**
-- `grease_summary.qo` — once per `report_interval_min` (outbound; default 24 h), templated, delivered in the Notecard's next scheduled outbound sync.
-- `grease_alert.qo` — fires whenever `fill_pct >= alert_threshold_pct` and the 1-hour cooldown has elapsed; `sync:true`; continues to fire hourly for as long as the interceptor remains above threshold.
-- **Inbound sessions** — the Notecard also wakes and polls Notehub every 2 hours (`HUB_INBOUND_MIN = 120`) to check for updated environment variables, independent of the outbound cadence. No application data is transmitted during an inbound-only session, but each session consumes radio energy (see §9).
-
-**Routed:** Both Notefiles land in Notehub. `grease_alert.qo` routes to whatever real-time notification channel the pumping provider uses for dispatch (service management platform webhook, email, SMS). `grease_summary.qo` routes to a long-term store for trend analysis and proactive scheduling intelligence.
-
-**Alert triggers on:** `fill_pct >= alert_threshold_pct` (default 75%) — level-triggered, not edge-triggered. A new alert fires each time the `ALERT_COOLDOWN_SEC` (1 hour) expires and the interceptor is still above threshold, so a persistently full interceptor generates repeated hourly alerts until it is serviced.
-
-### Example payloads routed to Notehub
-
-**Daily summary** (one per `report_interval_min`, templated, ~32 bytes):
-```json
-{
- "file": "grease_summary.qo",
- "body": {
- "fill_pct_avg": 42.3,
- "fill_pct_peak": 51.7,
- "fill_pct_now": 44.1,
- "valid_samples": 94,
- "power_mah": 248.6
- }
-}
-```
-
-**Threshold alert** (fires whenever `fill_pct >= threshold_pct` after cooldown, `sync:true`):
-```json
-{
- "file": "grease_alert.qo",
- "body": {
- "alert": "fill_threshold_exceeded",
- "fill_pct": 76.4,
- "threshold_pct": 75.0
- },
- "sync": true
-}
-```
-
-These Notefiles are separate so you can route them to different endpoints: summaries to a long-term analytics database, alerts to a real-time dispatch webhook or SMS gateway.
-
-## 9. Validation and Testing
-
-### Expected Steady-State Behavior
-
-On a correctly-behaving install, one `grease_summary.qo` event appears in Notehub every 24 hours and zero `grease_alert.qo` events appear (unless the interceptor is genuinely near full). The `valid_samples` field in the summary is the most useful commissioning diagnostic — after the first complete 24-hour reporting interval, a correctly-behaving unit should show roughly 96 valid samples at the default 15-minute interval. (The very first summary fires on cold boot with only the samples collected since power-on; do not treat a low count in that first event as a fault.) A count materially below 96 on subsequent days indicates intermittent sensor reads (cable length, probe positioning, or FOG reflectivity issues).
-
-If a summary Note is missing from Notehub for a given day, **do not immediately conclude that the firmware emitted nothing.** A Note that was correctly created on the device may not yet be visible in Notehub because the Notecard has not completed an outbound sync for that period, the device is temporarily offline or in a poor-signal location, queued Notes are still waiting for the next outbound window, or a Notehub route or forwarding step failed silently. Before diagnosing a firmware-level suppression, verify the device-side state first: query `hub.status` and `card.status` via the serial monitor or the in-browser Notecard Playground, or watch the USB serial debug output during a live wake cycle and look for a `note.add` request and a reply without an `err` field. If the Notecard confirms a successful `note.add` and shows a non-empty outbound queue, the firmware emitted the Note correctly — the gap is upstream (sync timing, connectivity, or routing), not in the firmware. Only if the outbound queue is empty and no `note.add` was attempted for the reporting window should you look for firmware-level suppression. In that case, the firmware suppressed the summary because `valid_samples` remained zero for the entire reporting window — the firmware gates `sendSummary` on `valid_samples > 0`, so a complete read failure produces no Note rather than a Note with `valid_samples: 0`. Check UART wiring, cable continuity, and the probe connector, and confirm that `interceptor_depth_mm` is not set shorter than the true sensor-to-surface distance at pump-out (a value set too low causes the firmware's `interceptor_depth_mm × 1.1` acceptance gate to reject every reading; see §7 sensor reading strategy).
-
-### Cold-Boot and Clock Sync
-
-On initial power-up the Notecard may not have synced its clock yet (`card.time` returns 0 until the first cellular session completes). The firmware allows at most one cold-boot summary and at most one cold-boot threshold alert before the clock is available, then suppresses further reports until time sync succeeds — preventing repeated low-count summaries or repeated threshold alerts from flooding Notehub before the device has a real timestamp. Once the Notecard syncs time, normal interval-based cadence resumes. If the device is in a poor-signal location and takes several wake cycles to establish its first session, no additional summaries or alerts will fire during that window; the fill accumulator continues running in the background and will be included in the first post-sync summary.
-
-**Speed up commissioning with a shorter sample interval.** The default 15-minute sample interval is correct for production but makes bench positioning slow. Before committing to final probe depth and cable dressing, temporarily set `sample_interval_sec` to `60` via a Notehub device-level environment variable (or by editing the `DEFAULT_SAMPLE_INTERVAL_SEC` constant in firmware during development). At 60 seconds per cycle you can watch several readings appear in the serial monitor within a few minutes and confirm stable fill-percentage values at multiple probe depths. Restore `sample_interval_sec` to `900` (or your production value) before leaving the site.
-
-**Sensor sanity check.** At first light, confirm the sensor is reading a plausible distance before deploying. With the interceptor hatch open and a tape measure in hand, plug the unit into a laptop via USB-C, set the Notecarrier CX's DIP switch to **HST** (exposes the Cygnet's USB serial port), and open a serial monitor at **115200 baud**. The firmware prints the raw median distance on every wake cycle:
-
-```
-[DBG] median distance mm: 423.00
-```
-
-Compare this value against your tape measurement from sensor face to the liquid surface. A raw distance discrepancy of more than 2–3 cm is a physical installation or signal issue — `interceptor_depth_mm` does not affect the raw distance reading (it is used only in the fill-percentage calculation and cannot shift what the sensor reports). Check instead: (1) the probe is centered directly over the chamber opening, not angled — off-center placement produces acoustic multipath off the interceptor walls and returns a longer-than-true distance; (2) the transducer face is pointing straight down with no tilt; (3) the sensor face is clean — grease fouling on the transducer attenuates the signal and causes the sensor to report a distance that is too long (fill appears lower than actual); (4) the UART wiring is correct — a wiring fault typically causes the read to be rejected entirely (no `[DBG]` line at all) rather than an offset reading. Once the raw distance matches the tape measurement within 1–2 cm, validate `interceptor_depth_mm` by comparing `fill_pct_now` in the first Notehub summary against your known reference: at 0% fill (immediately after pump-out) the distance should equal `interceptor_depth_mm` and `fill_pct` should read ~0%. Adjust `interceptor_depth_mm` via the Notehub environment variable if the derived percentage is off. **Note:** if `interceptor_depth_mm` is set shorter than the true pump-out distance, all readings will fall above the firmware's `interceptor_depth_mm × 1.1` acceptance gate and be silently rejected — no summary Note will appear until the value is corrected (see §7 sensor reading strategy). Correct it in Notehub and wait up to one inbound poll cycle (default 2 hours) for the update to take effect. Once the reading is plausible, close the hatch, return the DIP switch to **NC** for normal operation, and disconnect the laptop.
-
-**Alert simulation.** The fastest way to confirm the alert path end-to-end: in Notehub, lower `alert_threshold_pct` to `5` in the device's environment variables. On the next inbound sync (within 2 hours at the default `HUB_INBOUND_MIN`), the device will pull the new value. At the next wake, the current fill percentage will almost certainly exceed 5%, and a `grease_alert.qo` Note will arrive in Notehub within a session-establishment window of the sample time.
-
-**Mojo wiring and energy reporting.** The Mojo is a permanent part of the deployed unit. Wire it inline on the `+VBAT` rail between the 5 V DC wall adapter and the CX `+VBAT` pad (adapter positive lead → Mojo `BAT`; Mojo `LOAD` → CX `+VBAT` pad; adapter negative → CX `GND`), and connect the Mojo to the Notecarrier CX Qwiic port with a 4-pin JST-SH Qwiic cable. Because Mojo is inserted at the `+VBAT` rail, its readings reflect the **whole-board** draw — Notecard plus the CX's onboard regulators, quiescent biasing, and the continuously-powered A02YYUW sensor, not the Notecard in isolation. Notecard firmware v8.1.3 or later auto-detects the Mojo over I²C; no extra configuration is required. Each daily summary cycle the firmware calls [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power), reads the accumulated `milliamp_hours`, reports it as `power_mah` in the `grease_summary.qo` Note, then resets the counter so each summary reports the energy consumed during that one window. A `power_mah` of `-1` in a summary means the Mojo reading was unavailable for that window (check the Qwiic connection and the Notecard firmware version). To inspect live values during commissioning, issue `{"req":"card.power"}` in the in-browser Notecard Playground or over the USB serial connection and confirm a plausible `milliamp_hours` and `voltage`.
-
-The table below shows the published MBGLW Notecard figures alongside the trace shapes to look for and confirm on your assembled unit. Board-level numbers beyond the Notecard's own idle are assembly-dependent and should be validated empirically with Mojo rather than assumed from the figures below.
-
-| Phase | MBGLW Notecard published figure | Expected trace on your assembly (confirm with Mojo) |
-|---|---|---|
-| Notecard idle between syncs | ~8 µA ([low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)) — achievable when powered via +VBAT with VUSB absent, as in this build | Whole-board floor will exceed this: add CX regulator quiescent current, the continuously-powered A02YYUW (~8 mA), and any other peripheral leakage. The actual floor is assembly-dependent — confirm with Mojo. |
-| Host active (~2 seconds sampling cycle) | Notecard remains near idle; host MCU and UART activity are additional load | Brief current step above the quiescent floor every `sample_interval_sec` (default 15 minutes); the A02YYUW draw is already in the floor (sensor is always powered) — Cygnet wake adds its own incremental draw on top |
-| Notecard cellular session (LTE Cat-1 bis) | ~250 mA average, ≤2 A peak during transmit bursts ([MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | **Outbound** sync: 30–60 seconds elevation once per `report_interval_min` (default once per day). **Inbound** polls: shorter sessions every 2 hours (`HUB_INBOUND_MIN = 120`) — up to 12 additional radio wakes per day at default settings. Each inbound session has a radio-wake and session-establishment cost even though no summary data is delivered; at default cadence the 12 daily inbound sessions may collectively rival or exceed the single outbound session in energy. Budget both when sizing supply capacity. A continuously powered sensor (~8 mA × 24 h ≈ 192 mAh/day) likely remains the dominant daily energy consumer unless the `+3V3_OUT` rail is cut during host sleep — confirm with Mojo (splice between the DC wall adapter and CX `+VBAT` as described above). |
-
-The dominant daily energy consumer is likely the always-on sensor (~8 mA continuous ≈ 192 mAh/day) unless the `+3V3_OUT` rail is cut during host sleep — which the CX datasheet does not document. The cellular budget is composed of both the outbound summary sync (one per `report_interval_min`) and the inbound environment-variable polls (up to 12 per day at the default 2-hour cadence); at default settings the aggregated inbound sessions may collectively approach or exceed the energy of the single outbound sync. On a correctly-behaving Mojo trace you should see: a roughly constant quiescent floor (Notecard idle + always-on sensor), a brief incremental step every 15 minutes when the host is active, a pattern of shorter radio-wake spikes approximately every 2 hours (inbound polls), and a larger, longer cellular sync spike once per `report_interval_min` (outbound). If empirically the quiescent floor drops sharply between the 15-minute host-active steps, the sensor rail is being cut during sleep — confirm either way before sizing a battery-backed variant. If instead you see sustained elevated current with no sleeping pattern, the host is not sleeping — troubleshoot as follows for the Notecarrier CX: (1) confirm `NotePayloadSaveAndSleep` is actually reached (Notecard API debug output on the USB serial monitor stops after a successful sleep call); (2) confirm the `card.attn` request returns without an error in that same debug stream; (3) the Notecarrier CX switches the Cygnet host power rail via its built-in ATTN-controlled power circuit — no external EN pin wiring is needed or expected on this carrier. The Mojo remains wired inline on `+VBAT` and connected over Qwiic in the deployed unit, so the same energy trace is available in the field through the `power_mah` field of each daily summary.
-
-## 10. Limitations and Next Steps
-
-This reference design is scoped tightly to the indoor-utility-room HGI install and to geometries where the liquid surface rises with FOG accumulation. The simplifications below are deliberate — they keep the build buildable without a hazardous-location electrical review or a custom dual-sensor probe — and each has a clear extension path for a production rollout.
-
-### Simplified for the POC
-
-The simplifications below are deliberate scope choices — each keeps the build buildable without a hazardous-location review or a custom dual-sensor probe, and each carries a clear extension path for a production rollout.
-
-**Scoped to HGI and batch-collection geometries, not validated for conventional constant-level interceptors.** This reference design is scoped to hydromechanical and batch-collection interceptors without a fixed outlet weir — geometries where the top liquid surface rises with FOG and wastewater accumulation, making the fill percentage a meaningful pump-out indicator. On a **conventional constant-level gravity interceptor** (outlet weir holds the liquid surface at a fixed height regardless of FOG thickness), the top surface stays nearly constant as grease accumulates, so `fill_pct` will not track FOG buildup and the 75% alert threshold will not map to regulatory pump-out criteria. **Do not deploy this reference design on a conventional constant-level interceptor for compliance purposes without independent empirical validation** that surface elevation correlates with FOG accumulation in that specific unit. For conventional interceptors, direct FOG-layer measurement (e.g., differential float pair, bottom conductivity probe, or matched top-and-bottom ultrasonic sensors) is required.
-
-**Not rated for classified hazardous locations.** The A02YYUW probe, all associated wiring, and the electronics in this build are standard, non-intrinsically-safe (non-IS) components with no NEC Article 505/506 Class/Zone, ATEX, or IECEx certification. Whether a given interceptor installation constitutes a classified hazardous location is a site-specific determination that must be made by a qualified licensed electrician or AHJ before deployment — **do not assume an installation is unclassified without a documented review.** This reference design is suitable only for non-classified installations following a completed code and site-safety review. Classified hazardous-location installations require IS-rated sensors, certified IS barriers, explosion-proof (XP) or purged-and-pressurized (P) enclosures, and installation by personnel qualified for hazardous-area work.
-
-**Indoor utility-room power architecture only.** The documented hardware build assumes a 120 VAC wall outlet within ~1.5 m of the interceptor access cover, powering a UL-listed 5 V DC wall adapter that supplies the enclosure. A large fraction of commercial HGI installations are in rear yards, buried below a parking lot, or set in a floor vault where no convenient outlet exists. Those sites require a solar panel plus a lithium battery pack or a long conduit power run — neither is covered by this reference design. See the battery backup item in Production Next Steps below.
-
-**Single-layer measurement only.** A grease interceptor has two distinct layers: a floating FOG layer at the top and a settled sludge layer at the bottom. Regulatory compliance in many jurisdictions is based on the *combined* FOG-plus-sludge depth relative to the overall liquid depth, not just the FOG surface distance. The firmware measures one distance (sensor to FOG surface) and derives a simple fill percentage from it. A production deployment targeting jurisdictions with explicit combined-depth regulations would need either a second sensor at the bottom or a different measurement method to account for sludge accumulation.
-
-**Fixed interceptor depth per device.** `interceptor_depth_mm` is a single scalar representing the sensor-to-reference-surface distance when the interceptor is at 0% fill. This must be measured and set manually per installation; there is no auto-calibration. If the probe mounting depth shifts (e.g., if someone repositions the cover), the calibration drifts silently.
-
-**No FOG layer thickness calculation.** The sensor reports the distance to the *surface* of the FOG layer, not its thickness. Thickness (a more directly actionable metric) would require knowing the bottom of the FOG layer, which is the top of the wastewater zone below — measuring that would require a second sensor or a conductivity probe.
-
-**Environmental fouling not detected.** The A02YYUW probe is IP67 and tolerates condensation and splash, but a heavy grease coating on the transducer face will attenuate the ultrasonic signal and produce erroneous long readings (sensor thinks the surface is farther away than it is, i.e., fill level appears lower than actual). The firmware has no mechanism to detect or correct for probe fouling. A valid-samples count well below the expected 96/day can be a symptom, but is not a reliable diagnostic.
-
-**No temperature compensation beyond the sensor.** The A02YYUW has an internal temperature-compensation circuit for the speed of sound. The firmware does not read or log temperature separately — temperature is not transmitted to Notehub.
-
-**Alert cooldown is per-device, not per-channel.** The 1-hour alert cooldown is enforced in firmware. If the downstream routing endpoint needs its own deduplication or escalation logic (e.g., "send a second alert if not acknowledged within 4 hours"), that must be implemented in the Notehub route or the receiving system.
-
-### Production Next Steps
-
-Once the basic level monitor is running in the field, the following extensions are the natural progression — roughly from per-site usability to deeper measurement and fleet management.
-
-**A per-installation commissioning flow** removes the manual setup step: expose a `calibrate` environment variable that, when set, records the current sensor reading as the 0%-fill reference distance and stores it back as `interceptor_depth_mm`, replacing the manual tape-measure step with a firmware-driven calibration gesture.
-
-**Dual-layer measurement** addresses jurisdictions with combined-depth rules: add a conductivity or float probe near the bottom of the interceptor to detect sludge accumulation and report combined FOG+sludge fill depth.
-
-**Probe-fouling detection** turns an invisible failure into a leading indicator: add a signal-quality metric derived from the standard deviation of the 5-reading median filter. A high std-dev or elevated rejection rate is a leading indicator of fouling or multipath before the readings go completely invalid.
-
-**Over-the-air firmware updates** via [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) let threshold recipes and protocol updates be pushed to the entire fleet without a site visit.
-
-**Battery backup** via Blues Scoop or a small sealed lead-acid keeps interceptors in outage-prone locations protected, ensuring an overflow alert fires even during a kitchen's electrical fault.
-
-**Per-device pump-out logging** closes the service loop: add a `grease_service.qi` inbound Notefile that the dispatch system writes to when a pump-out event is completed, allowing the firmware to reset the fill accumulator on confirmed service and start fresh from a known-empty state.
-
-## 11. Summary
-
-The pumping provider who used to send a truck every four weeks regardless of fill now dispatches on condition: the FSE near 75% gets serviced before it overflows, the slow-season taqueria gets serviced less often without the compliance risk. An ultrasonic probe, a cellular Notecard, and a fill percentage that arrives in Notehub every day — plus an alert that wakes the radio whenever the threshold is crossed — turn a fleet of opaque grease traps into a route built on actual demand. The same firmware, the same enclosure, the same Notehub project deploys identically at every HGI on the route, independent of WiFi, IT policy, or building age.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/commercial-grease-interceptor-level-monitor/)**
diff --git a/58-commercial-grease-interceptor-level-monitor/banner.png b/58-commercial-grease-interceptor-level-monitor/banner.png
deleted file mode 100644
index 455cd71d..00000000
Binary files a/58-commercial-grease-interceptor-level-monitor/banner.png and /dev/null differ
diff --git a/58-commercial-grease-interceptor-level-monitor/diagrams/01-system-architecture.svg b/58-commercial-grease-interceptor-level-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index b089848c..00000000
--- a/58-commercial-grease-interceptor-level-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/58-commercial-grease-interceptor-level-monitor/diagrams/02-wiring-assembly.svg b/58-commercial-grease-interceptor-level-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 7c2200b8..00000000
--- a/58-commercial-grease-interceptor-level-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,105 +0,0 @@
-
diff --git a/58-commercial-grease-interceptor-level-monitor/diagrams/03-data-flow.svg b/58-commercial-grease-interceptor-level-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index 583c953b..00000000
--- a/58-commercial-grease-interceptor-level-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/60-lone-worker-panic-fall-detection-beacon/README.md b/60-lone-worker-panic-fall-detection-beacon/README.md
index bdb59ea7..e6c19e6e 100644
--- a/60-lone-worker-panic-fall-detection-beacon/README.md
+++ b/60-lone-worker-panic-fall-detection-beacon/README.md
@@ -1,592 +1,5 @@
-# Lone Worker Panic & Fall Detection Safety Beacon
+# Lone Worker Panic and Fall Detection Beacon
-
+A wearable cellular and satellite safety beacon for lone workers that detects falls and accepts a panic-button press, reaching dispatchers from remote locations where coverage is thin.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Lone%20Worker%20Panic%20%26%20Fall%20Detection%20Safety%20Beacon).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/lone_worker_beacon/](firmware/lone_worker_beacon/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a wearable [safety assurance](https://blues.com/safety-assurance/) device for utility linemen, oilfield pumpers, field service technicians, and solo contractors. A single [Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) carries cellular, WiFi, and Skylo NTN satellite radios on one module and fails over between them automatically. It turns a belt-clip enclosure into a cellular-first, satellite-backed distress beacon — detecting falls and accepting an explicit panic-button press — that can reach a dispatcher from the middle of nowhere, exactly where lone-worker incidents happen.
-
-## 1. Project Overview
-
-
-
-**Not a certified life-safety device.** This is a proof-of-concept reference design intended to demonstrate Blues hardware and firmware patterns. It is not a certified personal emergency response system, not a classified man-down or lone-worker protection device under any regulatory scheme, and has not been evaluated for fail-safe or safety-critical operation. It must **supplement, not replace** — established lone-worker safety procedures, mandatory check-in protocols, required PPE, and any regulatory or contractual safety obligations that apply to your operation. Never deploy this design as a sole means of worker protection.
-
-
-
-**The problem.** A utility lineman working an isolated substation, an oilfield pumper checking a remote wellhead at night, a field-service tech in a basement boiler room at 2 AM — each of these workers shares a common vulnerability: if something goes wrong, nobody will know for hours. Lone-worker incidents don't always announce themselves. Falls from elevation are silent. Heart events are silent. The worker simply stops moving, and no one notices until a shift check-in is missed or a buddy does a welfare call.
-
-The gap isn't awareness — most safety-conscious operations already require check-in procedures. The gap is *automatic, continuous* monitoring that doesn't depend on the worker remembering to push a button every 30 minutes. What these workers need is a device that monitors for the physical signatures of an incident — sudden free-fall, violent deceleration on impact, prolonged motionlessness after a fall, and raises an alarm without any action on the part of the worker. The panic button is a secondary escape valve: an explicit human override for situations where the physics don't look like a fall but the worker knows something is very wrong.
-
-This project is that device — a wearable safety beacon built on two core detection modes: automatic fall detection and explicit panic-button input. The onboard Cygnet STM32L433 host runs a two-stage fall-detection algorithm on the LIS3DH accelerometer, monitors a held-down panic button with debounce logic, and drives a haptic motor to acknowledge every confirmed event. The accelerometer is sampled at ~100 Hz: a 10-sample inner loop runs at 10-millisecond intervals so free-fall phases as short as 80 milliseconds are always observed, while Notecard I/O and state checks run at the outer ~10 Hz cadence (see [Section 7](#7-firmware-design) for the sampling design). On fall or panic, the firmware immediately queues a compact emergency Note carrying the Notecard's cached location and transmits it with `sync:true` — no GPS wait before the alert goes out. A non-blocking background GPS search then runs without suspending fall or button monitoring; if a fresh fix arrives within the timeout window, a follow-up `beacon_location.qo` Note is queued with the event-time coordinates. See [Section 7](#7-firmware-design) for the full two-Note flow.
-
-**Why Notecard.** Cellular coverage is not a given for the environments where lone-worker incidents happen. A substation at the edge of a service area, a gas compressor station in a rural county, a mine portal — these are precisely the places where a worker is most isolated *and* where cellular signal is most likely to be marginal or absent. Relying on cellular alone creates the dangerous assumption that signal is available when it's needed most.
-
-
-
-That's why [Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) (NOTE-NBGLWX) is the architectural foundation of this design, not a nice-to-have bolted on afterward. It carries three radios on one M.2 module — cellular (LTE-M / NB-IoT / GPRS), WiFi, and satellite over the [Skylo](https://www.skylo.tech/) non-terrestrial network (NTN) — and selects among them automatically. The cellular path covers the vast majority of activations — cellular is broadly deployed, even in surprisingly rural areas. But when cellular genuinely fails, the Skylo satellite link is there, on the same board: no companion module, no second device to wire in. Skylo covers supported regions (see the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for the current coverage footprint); within that footprint, a device with an unobstructed view of the sky can deliver a distress message to the [Blues Notehub](https://blues.com/notehub/) cloud service even when every terrestrial network is unavailable. Satellite coverage is not guaranteed in every no-cellular location; it depends on the Skylo coverage region, sky-view geometry, and antenna orientation. But for the substations, oilfields, and rural worksites this design targets, it provides the safety margin that cellular alone cannot.
-
-The firmware sets a single [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) preference of `wifi-cell-ntn`: the Notecard prefers WiFi where a provisioned AP is reachable, falls back to cellular (the de-facto primary for a roaming worker), and falls back again to Skylo satellite when every terrestrial network is unavailable. Failover happens inside the Notecard; the host firmware never branches on which network is live. The WiFi path is an opportunistic bonus — a field tech standing near a facility WiFi AP may sync an alert without any cellular usage at all — but it is only active when network credentials have been provisioned on the Notecard and a compatible AP is within range.
-
-**Deployment scenario.** The beacon ships as a self-contained unit in a rugged belt-clip enclosure, powered by a 3.7V LiPo battery. Workers clip it onto their belt or hard-hat band like a pager. Worker IDs are pre-provisioned per device in Notehub before deployment; changes to `worker_id` propagate to the device on the next inbound sync, which defaults to every 2 hours, not immediately at shift start. Falls and panics generate immediate alerts. No app, no phone pairing, no worker attention required — just clip it on and go.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The whole point of this device is that it must never miss the moment something goes wrong, so the Cygnet STM32L433 host on the Notecarrier CX never sleeps. It runs a dual-cadence loop instead: a fast inner loop reads the LIS3DH every 10 milliseconds (matching the sensor's 100 Hz ODR) while an outer ~10 Hz cadence handles Notecard I/O, GPS polling, the panic-button debounce, and the DRV2605L haptic feedback. The instant the two-stage algorithm confirms a fall — or the worker holds the button — the host queues the alert Note with the Notecard's cached location, triggers an immediate sync, and starts a non-blocking GPS search that runs in the background without ever pausing fall detection. If a fresh fix arrives within the window, a follow-up `beacon_location.qo` Note carries the event-time coordinates. All Notecard communication stays on I²C — no AT commands, no serial framing, no session management for the firmware to babysit.
-
-**Notecard responsibilities.** The Notecard holds the daily flush schedule via [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) but treats any `sync:true` alert Note as an immediate interrupt — the dispatcher hears about a fall in the same minute it happens, not at the next scheduled outbound window. The Notecard also owns the location story: once the host calls `card.location.mode`, the Notecard caches each GPS fix and embeds it in every subsequent compact template Note via `_lat`/`_lon`, so the firmware never has to pass coordinates in its Note body.
-
-When the worker walks into a coverage hole and cellular fails, Notecard for Skylo quietly switches to its onboard Skylo satellite (NTN) radio and tries the satellite path. The failover is transparent to both the firmware and to Notehub — the same Note, the same template, the same event structure arrives regardless of which radio carried it.
-
-**Notehub responsibilities.** [Notehub](https://notehub.io) ingests every Note across both transports, stores each event, and fans out to whatever dispatch system the operator has configured. `beacon_alert.qo` events and their paired `beacon_location.qo` follow-ups land at the dispatcher's real-time endpoint. [Environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) flow the other direction on each inbound sync — a safety supervisor can retune the free-fall threshold or change a worker ID from the Notehub console without ever opening an enclosure.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — this project ships no specific downstream endpoint. A typical deployment routes both `beacon_alert.qo` and `beacon_location.qo` to the same real-time dispatch endpoint, joined by device UID and `event_id`.
-
-## 3. Technical Summary
-
-**What you'll have when you're done:** a wearable beacon that detects falls and panic-button presses, transmits alerts to Notehub over cellular or satellite, and provides haptic feedback on every event.
-
-**Prerequisites:** Arduino IDE or `arduino-cli`, a Notehub account, and a Notecarrier CX fully assembled per the wiring in [Section 5](#5-wiring-and-assembly).
-
-**Step 1: Install dependencies**
-
-```bash
-# Install the STM32 core
-arduino-cli core install STMicroelectronics:stm32
-
-# Install required libraries
-arduino-cli lib install "Blues Wireless Notecard" \
- "SparkFun LIS3DH Arduino Library" \
- "Adafruit DRV2605 Library" \
- "Adafruit BusIO"
-```
-
-**Step 2: Configure and compile**
-
-1. Open `firmware/lone_worker_beacon/lone_worker_beacon.ino` in the Arduino IDE or a text editor.
-2. In `lone_worker_beacon_helpers.h`, find the line `#define PRODUCT_UID` and replace the placeholder with your Notehub project's ProductUID (from [notehub.io](https://notehub.io) under project settings).
-3. Compile (the FQBN below matches `firmware/lone_worker_beacon/sketch.yaml`, which the Arduino IDE picks up automatically when invoked from the sketch directory):
-
-```bash
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/
-```
-
-**Step 3: Flash to Notecarrier CX**
-
-```bash
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/ttyUSB0 firmware/
-```
-
-(Replace `/dev/ttyUSB0` with your platform's serial port; use `COM3` on Windows, find the port in Arduino IDE's Tools menu.)
-
-**Step 4: Test and verify**
-
-- Power the beacon; the Notecard claims itself to your project on first sync.
-- Hold the panic button for 2+ seconds. You should feel a triple haptic buzz (alert queued).
-- Check Notehub: navigate to your project's Devices tab, select your device, and view the Events log. A `beacon_alert.qo` event with `"type":"panic"` should appear within 30–90 seconds.
-- Drop the beacon 50–80 cm onto a padded surface. You should feel a double buzz (fall detected) and see a `beacon_alert.qo` with `"type":"fall"` in the Events log.
-
-If no event appears, check [Section 10 (Troubleshooting)](#10-troubleshooting).
-
-Here is a sample Note this device emits:
-
-```json
-{
- "uid": "d84e3a...",
- "device": "dev:000000000000000",
- "file": "beacon_alert.qo",
- "received": 1714582020,
- "best_lat": 40.71280,
- "best_lon": -74.00601,
- "best_location": "New York NY",
- "body": {
- "type": "fall",
- "worker_id": "lineman-042",
- "event_id": 7,
- "voltage": 3.71,
- "loc_age_s": 142
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host — no separate MCU needed. I²C, SPI, analog, and GPIO headers support the full sensor stack. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)) | 1 | One M.2 module carrying cellular (LTE-M / NB-IoT / GPRS, Quectel BG95-S5 modem), WiFi (Silicon Labs WFM200S), and Skylo satellite (NTN) radios, plus an integrated GPS/GNSS. Seats in the Notecarrier CX's M.2 slot. The firmware's `card.transport` `wifi-cell-ntn` setting makes it prefer WiFi, fall back to cellular, and fall back again to the Skylo satellite network when every terrestrial network is unavailable — no companion module, no second device, automatic failover. Requires the antennas below. |
-| Skylo-certified LTE/satellite antenna included with Notecard for Skylo (u.FL) | 1 | Connects to the `MAIN` u.FL port and carries **both** the terrestrial cellular signal and the Skylo satellite link — a single antenna for both networks. Use only the Skylo-certified antenna supplied with Notecard for Skylo; substituting an uncertified antenna risks regulatory non-compliance and link failure. A belt-worn beacon needs this antenna where it can see the sky: position it against the top (sky-facing) wall of the polycarbonate enclosure (polycarbonate is RF-transparent, so no external routing or bulkhead is required), and in the northern hemisphere a southward orientation improves Skylo link margin. The same placement that enables satellite fallback serves cellular as well. |
-| Passive GPS/GNSS antenna (u.FL) per the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | 1 | Connects to the `GPS` u.FL port for the Notecard's own GNSS time/location — this is the device's location source, and `card.location` draws from it for the coordinates embedded in alert Notes. Adhere it alongside the main antenna on the top (sky-facing) interior wall of the polycarbonate enclosure for best acquisition geometry during GPS-on events. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb-counter on the LiPo rail for ground-truth current and energy measurement during bench validation. |
-| [SparkFun Triple Axis Accelerometer Breakout — LIS3DH (SEN-13963)](https://www.sparkfun.com/products/13963) | 1 | 3-axis MEMS accelerometer at 0x18 on I²C. Configured at 100 Hz ODR (10 milliseconds hardware sample period). The firmware samples at ~100 Hz via a 10-sample inner loop (one read every 10 milliseconds), matching the sensor ODR and reliably catching free-fall phases as short as 80 ms. ±4g range provides headroom for both normal impacts and genuine falls. Built-in free-fall and shock interrupt hardware is a production upgrade path. |
-| [Adafruit DRV2605L Haptic Motor Controller (#2305)](https://www.adafruit.com/product/2305) | 1 | I²C haptic driver with 123 built-in waveform effects. Drives the ERM motor directly; no transistor or PWM circuit needed. Supports both ERM and LRA motors. |
-| [Adafruit Vibrating Mini Motor Disc (#1201)](https://www.adafruit.com/product/1201) | 1 | Small flat ERM disc motor. Gives distinct, wrist-perceptible confirmation buzzes at fall and panic events. Wires directly to the DRV2605L output terminals. |
-| [SparkFun Momentary Push Button Switch — 12mm Square (COM-09190)](https://www.sparkfun.com/products/9190) | 1 | Panic input, wired to D9 with firmware INPUT_PULLUP. Choose a cap that can be operated with a gloved hand for field deployability. |
-| 3.7V LiPo battery, 1200 mAh, JST-PH 2-pin (e.g. [Adafruit #258](https://www.adafruit.com/product/258)) | 1 | Powers the device. Runtime depends on alert frequency and transport conditions — validate with Mojo before sizing for deployment. Use a cell with built-in protection circuitry. |
-| [Adafruit JST PH 2-Pin Cable — Female Connector 100mm (#261)](https://www.adafruit.com/product/261) | 1 | Adapts the LiPo cell's JST-PH male plug to bare wire leads for connection to the Mojo `BAT+` and `GND` solder pads. Required to complete the LiPo→Mojo power path without cutting or modifying the LiPo cell's factory connector. |
-| Polycarbonate project enclosure, ≥120 × 80 × 40 mm interior, IP54+ (e.g., Hammond 1553JGYBK) | 1 | Protects the board stack, battery, and connectors in a field-deployable package. The Notecarrier CX (83 × 63 mm) sets the minimum floor area. **Must be polycarbonate (non-metal)** — metal enclosures block cellular, WiFi, GNSS, and satellite signals. Requires cutouts for the panic button and the LiPo JST connector for battery access; Notecard for Skylo's `MAIN` and `GPS` u.FL antennas adhere to the interior enclosure walls without external routing. IP54 or better is recommended for outdoor field use. Verify interior dimensions against your specific board-and-battery stack before ordering. **No charging circuit is included in this BOM**. See [Section 9](#11-limitations-and-next-steps) for rationale; plan enclosure cutouts for any charging connector you add downstream. |
-
-Notecard for Skylo ships with an active global SIM including 500 MB of cellular data and 10 years of service, **plus** 10 KB of bundled Skylo satellite data — no activation fees, no monthly commitment, and no separate satellite provider subscription. Additional satellite data is billed per byte (see the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for current pricing).
-
-
-
-**Installer-supplied, deployment-specific item.** Belt clip or wearable mounting hardware — attaches the beacon to a worker's belt, hard-hat band, or safety vest. Select a clip rated for the enclosure weight (~300 g fully loaded). Some enclosure families include an optional clip-arm accessory; a spring-steel belt clip can be mounted to the enclosure exterior with M3 screws.
-
-**Charging and power access.** No LiPo charging circuit, dock, or power-switch hardware is included in this BOM or wiring. The project runs on a bare LiPo cell until depleted. A production wearable needs a USB-C LiPo charging circuit integrated into the enclosure, plus overcharge and short-circuit protection if not already provided by the cell's built-in circuitry. See [Section 9](#11-limitations-and-next-steps) for details; adding a charging path is the expected next step for anyone moving from bench validation toward a field-deployed unit.
-
-
-
-## 5. Wiring and Assembly
-
-
-
-The whole stack — Notecarrier CX, Notecard for Skylo, accelerometer, haptic driver, and panic button — has to fit inside a belt-clip enclosure that a worker will forget they're wearing. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin headers; Notecard for Skylo seats into the M.2 slot and talks to the Cygnet host over the carrier's internal I²C. Because cellular, WiFi, and Skylo satellite all live on that one module, there is no companion satellite device to wire in — only the two u.FL antennas described below. The Mojo sits inline between the LiPo JST connector and the Notecarrier CX `+VBAT` pad during bench validation.
-
-All I²C peripherals (LIS3DH and DRV2605L) share the SDA/SCL bus exposed on the Notecarrier CX headers. On-board pull-ups are provided by the carrier; no external resistors are needed for the I²C lines.
-
-Pin-by-pin:
-
-- **+3V3** → LIS3DH `VIN`, DRV2605L `VIN` (both are 3.3V-native; see breakout datasheets for exact voltage range).
-- **GND** → LIS3DH `GND`, DRV2605L `GND`, one leg of the panic button.
-- **SDA** → LIS3DH `SDA`, DRV2605L `SDA`.
-- **SCL** → LIS3DH `SCL`, DRV2605L `SCL`.
-- **D9** → other leg of the panic button (firmware uses `INPUT_PULLUP`; active-low).
-- **DRV2605L `MOTOR+` / `MOTOR-`** → Adafruit Vibrating Motor Disc red and blue wires (polarity matches the driver output; swap if motor doesn't run).
-- **Power path (LiPo → Mojo → Notecarrier CX).** The Mojo sits inline on the battery rail as a coulomb counter. Wire it as follows:
- 1. Plug the JST-PH female pigtail (BOM item) onto the LiPo's JST-PH male connector. This gives you two bare wire leads: `+` (red) and `−` (black).
- 2. **LiPo `+` lead** → Mojo `BAT+` solder pad (or screw terminal marked `BAT`).
- 3. **LiPo `−` lead** → Mojo `GND` solder pad. The Mojo's `GND` terminal is common to both the battery-negative rail and the load-negative rail.
- 4. **Mojo `LOAD+` output** → Notecarrier CX **`+VBAT`** pad (completes the positive supply to the board stack).
- 5. **Mojo `GND` terminal** → Notecarrier CX **`GND`** header pin (completes the ground-return path; without this connection the circuit is open).
-- **Notecard for Skylo antennas** → `MAIN` u.FL port → included Skylo-certified antenna (carries both cellular and satellite); `GPS` u.FL port → passive GPS/GNSS antenna. Both adhere to the top (sky-facing) interior wall of the polycarbonate enclosure (see the antenna Note below). No companion satellite module and no JST cable are involved — all three radios are on Notecard for Skylo itself.
-
-
-
-**Antenna placement and connector mapping.** Notecard for Skylo uses two u.FL ports. Connect the included Skylo-certified antenna to the `MAIN` port — it carries **both** the cellular signal and the Skylo satellite link, so a single antenna serves both networks — and connect the passive GPS/GNSS antenna to the `GPS` port. Adhere both elements to the top (sky-facing) interior wall of the polycarbonate enclosure: polycarbonate is RF-transparent, so no external pigtail routing or bulkhead is needed. A belt-worn beacon only reaches the Skylo satellite network when its `MAIN` antenna can see the sky, so orient that wall upward when the unit is worn; in the northern hemisphere a southward orientation improves Skylo link margin. Use only the Skylo-certified antenna supplied with Notecard for Skylo on the `MAIN` port — substituting an uncertified antenna risks regulatory non-compliance and link failure. The `GPS` port is the location source for all `card.location` data embedded in alert Notes.
-
-**Charging is out of scope for this POC.** No charging circuit, dock, or inductive coil is wired in this build. Route the LiPo JST connector to an accessible point on the enclosure wall so the battery can be swapped or a bench charger connected without fully disassembling the unit. Do not seal the JST connector inside the enclosure with no external access path.
-
-
-
-The LIS3DH's SDO/SA0 pin sets the I²C address. Leave it unconnected or pulled to GND for address 0x18 (the firmware default). The DRV2605L address (0x5A) is fixed; no conflict.
-
-## 6. Notehub Setup
-
-**Detailed walkthrough for first-time users:**
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io). Click **Create a Project** → name it (e.g., "Lone Worker Beacons") → select your region → confirm. Copy the **ProductUID** displayed on the project details card. Paste this into `firmware/lone_worker_beacon/lone_worker_beacon_helpers.h` as the `PRODUCT_UID` macro value before flashing.
-
-2. **Claim the Notecard.** Power the beacon with a Notecard inserted and provisioned SIM. On first cellular or satellite session the Notecard associates with your project automatically. Verify in Notehub: navigate to **Devices** → click on your device's UID — it should appear within 30–90 seconds.
-
-3. **Complete the initial non-NTN sync.** Before any Skylo satellite (NTN) transmission is possible, Notecard for Skylo must complete at least one successful cellular or WiFi sync to associate with your project and register the compact template definitions in Notehub. The firmware's `card.transport` `wifi-cell-ntn` setting performs that first sync over cellular/WiFi automatically, so commission each beacon where it has terrestrial coverage — even if it will routinely operate over satellite — and confirm a sync lands in Notehub before deploying. See the [`card.transport` API reference](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) for details.
-
-4. **Create a Fleet per region or team.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. A natural structure is one fleet per crew or site — all devices in a fleet share the same detection thresholds, with per-device worker ID overrides. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can route a device to a different fleet automatically based on its reported location if workers cross territories.
-
-5. **Set environment variables.** All variables below are optional; firmware defaults apply until overridden. To set an env var in Notehub: open your project → **Devices** tab → click your device → **Environment** → **Fleet Environment** (applies to all devices in the fleet) or **Device Environment** (overrides fleet settings for this device alone) → add key-value pairs. Values propagate to the Notecard's local cache on the next inbound sync (default every 2 hours), then to the host on the following `env.get` poll (also up to 2 hours). Total propagation time: up to 4 hours. No re-flashing is required.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `worker_id` | `worker-001` | Human-readable worker or device identifier included in every Note. Maximum 24 characters; longer values are silently truncated on-device to preserve compact-packet payload size. Pre-provision this per device before deployment; changes propagate to the device on the next inbound sync (up to 2 hours by default), not immediately at shift start. |
- | `freefall_g` | `0.55` | Total acceleration magnitude (g) below which free-fall phase is declared. Clamped to 0.10–0.90 g. |
- | `impact_g` | `2.5` | Total acceleration magnitude (g) above which fall impact is confirmed. Clamped to 1.50–8.00 g. |
- | `fall_window_ms` | `500` | Milliseconds after a free-fall episode during which an impact must be detected to confirm the fall. Clamped to 100–2000 ms. |
- | `freefall_min_ms` | `80` | Minimum milliseconds the device must remain in free-fall before the impact window opens. Shorter free-falls (stumbles, tool drops) are ignored. Clamped to 20–500 ms. |
- | `panic_hold_ms` | `2000` | Milliseconds the button must be held before a panic alert fires. Prevents accidental triggers from gloved hands. Clamped to 500–10000 ms. |
-
-6. **Configure routes** (optional for now; use for live dispatch). Go to **Routes** → **Create Route** → Name it (e.g., "Dispatch Alerts") → select **Event** → filter by file `beacon_alert.qo` → select a destination (HTTP, SMTP, Slack, etc.). **Important:** Add a **second route** for `beacon_location.qo` to the **same** destination. When a fresh GPS fix arrives during the 90-second background search, `beacon_location.qo` carries event-time coordinates that supersede the cached location in the paired `beacon_alert.qo`. Downstream systems must pair the two Notes by `(device, event_id)` — `event_id` resets to 0 on each power cycle and is not globally unique on its own. The `(device, event_id)` join key is unambiguous across repeated alerts, retries, and network reordering.
-
-## 7. Firmware Design
-
-The firmware is split into three small files so the safety-critical detection paths stay legible. All three live directly under `firmware/`:
-- [`firmware/lone_worker_beacon/lone_worker_beacon.ino`](firmware/lone_worker_beacon/lone_worker_beacon.ino) — constants, globals, `setup()`, `loop()`.
-- [`firmware/lone_worker_beacon/lone_worker_beacon_helpers.h`](firmware/lone_worker_beacon/lone_worker_beacon_helpers.h) — shared `#define` constants, `extern` declarations, env-var clamp ranges, function prototypes.
-- [`firmware/lone_worker_beacon/lone_worker_beacon_helpers.cpp`](firmware/lone_worker_beacon/lone_worker_beacon_helpers.cpp) — all helper function implementations.
-
-**Dependencies:**
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)) — installed via Arduino IDE Boards Manager.
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (the `note-arduino` library). Install via Arduino Library Manager: `arduino-cli lib install "Blues Wireless Notecard"`.
-- [`SparkFun LIS3DH Arduino Library`](https://github.com/sparkfun/SparkFun_LIS3DH_Arduino_Library). Install via Library Manager: `arduino-cli lib install "SparkFun LIS3DH Arduino Library"`.
-- [`Adafruit DRV2605 Library`](https://github.com/adafruit/Adafruit_DRV2605_Library) and its `Adafruit BusIO` dependency. Install via Library Manager.
-
-### Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard configuration (`hub.set` + one-time `card.transport` `wifi-cell-ntn` for cellular→satellite fallback) at boot; returns fault state | `notecardConfigure()` |
-| Compact template registration (2 templates, retried; returns fault state) | `defineTemplates()` |
-| Environment variable refresh with clamp validation; called at boot and every 2 h | `fetchEnvVars()` |
-| LIS3DH setup (100 Hz, ±4g) | `initAccel()` |
-| DRV2605L setup (ERM library 1) | `initHaptic()` |
-| Two-stage fall detection | `pollFallDetection()` |
-| Hold-to-confirm panic button | `checkPanicButton()` |
-| Start non-blocking GPS search after an alert | `beginGpsSearch()` |
-| Advance GPS search; queue `beacon_location.qo` on fresh fix | `pollGpsSearch()` |
-| Alert Note with immediate sync (cached location + `loc_age_s` + `event_id`) | `sendAlert()` |
-| Arm non-blocking haptic pulse sequence | `triggerHaptic()` |
-| Advance haptic state machine (called every loop pass) | `pollHaptic()` |
-
-### Fall Detection Algorithm
-
-The firmware uses a two-stage software algorithm sampled at ~100 Hz: ten `pollFallDetection()` calls run per outer loop pass, spaced 10 milliseconds apart in a fast inner loop (matching the LIS3DH's 100 Hz ODR). At this rate the 80 milliseconds minimum free-fall duration guard (`DEFAULT_FREEFALL_MIN_MS`) spans ~8 consecutive samples, providing meaningful noise rejection. Notecard I/O, GPS polling, and haptic state advance once per outer pass at the ~10 Hz outer cadence. A single-stage threshold check (just watching for a spike) generates too many false positives from everyday bumps; requiring a free-fall phase before the impact check reduces nuisance alerts from walking into a doorframe or dropping a tool.
-
-**Stage 1 — Free-fall.** On each of the 10 inner-loop reads (spaced 10 milliseconds apart), the firmware computes total acceleration magnitude: `|a| = √(ax² + ay² + az²)`. When total-g drops below `freefall_g` (default 0.55g) and stays there for at least `freefall_min_ms` (default 80ms, remotely configurable via Notehub), the firmware exits Stage 1 and opens an impact-watch window. At ~100 Hz sampling the 80 milliseconds guard spans ~8 consecutive readings — meaningful noise rejection. A genuine free-fall from ~30 cm bench height lasts well over 100 ms.
-
-**Stage 2 — Impact.** Within `fall_window_ms` (default 500ms) of the free-fall phase ending, if total-g exceeds `impact_g` (default 2.5g), the fall is confirmed. The window closes automatically if no impact arrives — preventing a brief stumble or a tool being set down from generating a false alert.
-
-### Event Payload Design
-
-Both Notefiles use [compact templates](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) with `"format":"compact"`. This is a hard requirement for Skylo NTN (satellite) transport — compact Notes use fixed-length binary encoding, keeping the satellite packet well inside the 256-byte maximum payload limit and minimizing per-message satellite cost. The `_lat`/`_lon` keywords in the template body instruct the Notecard to embed its cached GPS location automatically; the firmware does not need to explicitly pass coordinates in the `note.add` body.
-
-Both templates include an `event_id` field — a monotonic counter incremented once per new alert dispatch, **not** per retry. The same `event_id` value appears in both `beacon_alert.qo` and its paired `beacon_location.qo`. Downstream systems must use `(device, event_id)` as the join key — `event_id` resets to 0 on each power cycle and is not globally unique on its own. The scoped `(device, event_id)` pair is stable across repeated alerts of the same type, across retries, and across network reordering.
-
-Example `beacon_alert.qo` event as it appears in Notehub (initial alert, cached location):
-
-```json
-{
- "uid": "d84e3a...",
- "device": "dev:000000000000000",
- "file": "beacon_alert.qo",
- "received": 1714582020,
- "best_lat": 40.71280,
- "best_lon": -74.00601,
- "best_location": "New York NY",
- "body": {
- "type": "fall",
- "worker_id": "lineman-042",
- "event_id": 7,
- "voltage": 3.71,
- "loc_age_s": 142
- }
-}
-```
-
-Example `beacon_location.qo` follow-up event (fresh event-time fix, sent only if GPS acquires within 90 seconds):
-
-```json
-{
- "uid": "d84e3b...",
- "device": "dev:000000000000000",
- "file": "beacon_location.qo",
- "received": 1714582087,
- "best_lat": 40.71294,
- "best_lon": -74.00589,
- "best_location": "New York NY",
- "body": {
- "type": "fall",
- "worker_id": "lineman-042",
- "event_id": 7
- }
-}
-```
-
-The `type` field in `beacon_alert.qo` carries one of two values: `fall` (two-stage algorithm confirmed) or `panic` (button held). `loc_age_s` is the age in seconds of the Notecard's cached GPS fix at the moment the alert was queued (−1.0 if no fix was available). `event_id` resets to 0 on each power cycle; downstream correlation should use `(device, event_id)` as the join key. When `beacon_location.qo` is also present (matching `event_id`), its coordinates supersede the cached location in `beacon_alert.qo` for mapping and dispatch response.
-
-### Low-Power Strategy
-
-**This POC keeps the host MCU awake continuously — this is a deliberate design choice, not an oversight.** The device must detect a fall or button press at any moment, so `card.attn` sleep mode (which cuts host power entirely) is incompatible with the monitoring requirement. Instead, the host MCU remains awake continuously, running a dual-cadence loop: the LIS3DH is sampled at ~100 Hz via a 10-sample inner loop (10 milliseconds between reads), and Notecard/GPS/haptic state advances at the outer ~10 Hz cadence. The inner-loop delays pace the outer cadence without a separate top-level `delay()` call. The battery-life penalty is real and significant: the STM32L433 at its default clock draws approximately 10–15 mA active, making host idle the dominant draw at steady state. See the [power validation table](#9-validation-and-testing) for per-state figures and validate total runtime with Mojo before sizing a battery for deployment.
-
-Notecard for Skylo runs in `periodic` mode with `outbound: 1440` (daily flush) and `inbound: 120` (2-hour environment-variable refresh). `notecardConfigure()` also issues a one-time `card.transport` `wifi-cell-ntn` so the Notecard prefers WiFi, then cellular, then Skylo satellite (NTN) — the failover is handled inside the Notecard, with no firmware branching. All emergency Notes carry `sync:true`, which bypasses the outbound interval and triggers an immediate session over whichever radio is reachable — the daily flush is a backstop for any queued data that sync:true did not deliver. Between sessions the Notecard sits in its own low-power idle state (~8 µA), regardless of which radio it last used. GPS is off by default and turned on only during alert events (fall, panic) — continuous GPS would consume an additional 30+ mA and is unnecessary given the design's event-driven location update cadence.
-
-A production implementation should use STM32L433 low-power STOP2 mode with GPIO wakeup on the LIS3DH hardware interrupt (INT1) and button pin — dropping host idle to ~2–3 µA while still catching every fall event in real time. See [Limitations](#11-limitations-and-next-steps).
-
-### Retry and Error Handling
-
-**Startup path.** Both `notecardConfigure()` and `defineTemplates()` return a boolean. `notecardConfigure()` retries `hub.set` up to five times to handle the cold-boot I²C race. `defineTemplates()` retries each of the two template registrations (`beacon_alert.qo`, `beacon_location.qo`) up to three times. If either function returns false, `g_setupFault` is latched in `setup()`, and a distinctive slow double-buzz haptic pattern fires so a misconfigured device is obvious at power-on without needing debug serial. A missing or empty `PRODUCT_UID` is also caught at runtime and added to the fault latch.
-
-A startup fault — missing or empty `PRODUCT_UID`, `hub.set` failure after five retries, any template registration failure, or LIS3DH initialization failure — is treated as unrecoverable. After the slow double-buzz pattern fires, `setup()` enters an infinite halt loop that repeats the fault buzz every 5 seconds; the beacon never enters `loop()` and no alerts are ever sent. This hard-stop behavior is intentional for a safety device: arming a beacon whose Notes cannot route, or whose compact Skylo NTN transport may be broken, gives a false sense of protection. The recurring buzz makes an unconfigured device unmistakably obvious at power-on — verify `PRODUCT_UID`, Notecard connectivity, and template registration, then power-cycle before deploying.
-
-**Sensor degradation.** `initAccel()` and `initHaptic()` each return a boolean; the main loop checks `g_accelReady` and `g_hapticReady` before calling the relevant functions. A missing or unresponsive LIS3DH at boot is treated as a hard fault — `g_setupFault` is latched alongside `g_accelFaultLatched`, the distinctive slow double-buzz fault pattern fires, and `setup()` enters the infinite halt loop. This ensures a beacon that cannot perform fall detection is immediately obvious at power-on and cannot be silently deployed as panic-only. A missing DRV2605L skips haptic feedback without crashing the loop. An accelerometer fault that develops after boot (consecutive bad reads beyond `ACCEL_FAIL_THRESHOLD` after `ACCEL_REINIT_MAX` failed reinitialisations) latches `g_accelFaultLatched` and clears `g_accelReady`, permanently disabling fall detection until power-cycle. The device then emits a single buzz every 30 seconds so an operator can recognize the unit has dropped to degraded panic-only mode.
-
-**Notecard response errors.** All `requestAndResponse()` calls check for a `NULL` response and call `notecard.responseError()` before accessing fields; failed transactions are skipped and the loop continues with stale values. `note.add` failures for alerts are retried up to `ALERT_RETRY_MAX` (3) times with 500 milliseconds spacing via the non-blocking retry queue; each retry attempt carries the original `event_id` and `loc_age_s` so no context is lost.
-
-**Important — queueing failure vs. delivery failure.** Only Notes that `note.add` *successfully accepts* are stored inside the Notecard and retried by the Notecard for cellular or satellite delivery. If the firmware's retry budget is exhausted before `note.add` succeeds, for example because the Notecard is temporarily unreachable on I²C — the alert is **dropped** and is never delivered to Notehub. The firmware logs this via `DEBUG_PRINTLN` but takes no further action. If stronger guarantees are required, persist unsent alerts in non-volatile storage until `note.add` succeeds, or trigger a local fault (e.g., a distinctive haptic pattern) when the retry budget is exceeded. An operator who suspects a missed alert should issue `{"req":"hub.status"}` from the blues.dev In-Browser Terminal to check the last sync time, pending Note count, and transport-layer error.
-
-**GPS acquisition.** The non-blocking GPS state machine polls `card.location` at 2-second intervals (throttled from the 10 Hz loop rate) for up to `DEFAULT_GPS_TIMEOUT_SEC` (90 seconds). Before the alert Note is queued, the firmware captures the current cache epoch into a per-alert local variable (`thisCacheEpoch`); this value is passed to `sendAlert()` to compute `loc_age_s` and, when no GPS search is already active, is copied into `g_gpsCacheEpoch` (the freshness baseline the search uses). A fix is accepted only when its epoch post-dates that baseline, preventing a stale cached fix from being mistaken for a fresh acquisition. On a fresh fix, a `beacon_location.qo` Note is queued immediately with `sync:true`. The timeout check uses elapsed time (`millis() - start >= interval`) rather than an absolute deadline to remain correct across the 49.7-day `millis()` rollover. If no fresh fix arrives within the timeout, GPS is disabled and only the initial `beacon_alert.qo` stands. Only one GPS enrichment window can be active at a time — if a second alert fires during the 90-second window (possible because the 60-second cooldown is shorter than the GPS timeout), `beginGpsSearch()` returns immediately and the second alert receives its cached location only; no `beacon_location.qo` is queued for it.
-
-**Alert rate limiting and suppression.** `DEFAULT_ALERT_COOLDOWN_SEC` (60 seconds) gates fall and panic alerts. If a fall or panic event occurs within 60 seconds of the previous alert, no Note is queued and no GPS acquisition is attempted. A suppressed fall produces no local indication; a suppressed panic produces a single haptic buzz so the worker knows the hold was registered (distinct from the triple-buzz that confirms an alert was accepted). This prevents alert storms from a tumbling device without requiring any worker action.
-
-### Key Code Snippet 1: Compact Template with _lat/_lon
-
-The `format: "compact"` and `port` arguments are required for Skylo NTN (satellite) transport. The `_lat`/`_lon` template fields tell the Notecard to embed its cached best-available location (GNSS or cell-derived) in the compact packet automatically — the firmware never passes coordinates in the Note body. The `event_id` field uses type hint `14` (4-byte signed int32, per the Blues compact-template encoding table) and is written to both `beacon_alert.qo` and `beacon_location.qo` with the same value; downstream systems join the two Notes using `(device, event_id)` as the key.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "beacon_alert.qo");
-JAddNumberToObject(req, "port", 50);
-JAddStringToObject(req, "format", "compact");
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "type", "s"); // variable-length string hint
-JAddStringToObject(body, "worker_id", "s");
-JAddNumberToObject(body, "event_id", 14); // 4-byte signed int32 correlation key
-JAddNumberToObject(body, "voltage", 14.1); // 4-byte float
-JAddNumberToObject(body, "_lat", 14.1); // GPS lat from Notecard cache
-JAddNumberToObject(body, "_lon", 14.1); // GPS lon from Notecard cache
-notecard.sendRequest(req);
-```
-
-### Key Code Snippet 2: Immediate-Sync Alert
-
-`sync:true` tells the Notecard to attempt a session immediately, bypassing the periodic outbound interval. Notecard for Skylo selects the radio per its `card.transport` `wifi-cell-ntn` preference; if WiFi and cellular both fail, the Note routes over Skylo satellite automatically.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "beacon_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "type", "fall");
-JAddStringToObject(body, "worker_id", "lineman-042");
-JAddNumberToObject(body, "event_id", (double)thisEventId); // monotonic; matches beacon_location.qo
-JAddNumberToObject(body, "voltage", 3.71);
-notecard.sendRequest(req);
-```
-
-### Key Code Snippet 3: Two-Stage Fall Confirmation
-
-Free-fall (low-g) followed by impact (high-g) within a short window. Both must occur in sequence; either alone does not confirm a fall.
-
-```cpp
-float totalG = sqrtf(ax*ax + ay*ay + az*az);
-
-// Stage 1: free-fall phase — total-g drops below threshold for minimum duration
-if (!g_inFreefall && totalG < g_freefallG) {
- g_inFreefall = true;
- g_freefallStart = millis();
-} else if (g_inFreefall && totalG >= g_freefallG) {
- if ((millis() - g_freefallStart) >= g_freefallMinMs) {
- g_watchingImpact = true;
- g_impactWindowStart = millis(); // record start; compare elapsed (wraparound-safe)
- }
- g_inFreefall = false;
-}
-
-// Stage 2: impact detection — high-g spike within the window
-if (g_watchingImpact) {
- if ((millis() - g_impactWindowStart) >= g_fallWindowMs) {
- g_watchingImpact = false; // window expired — not a fall
- } else if (totalG > g_impactG) {
- g_watchingImpact = false;
- return true; // confirmed fall
- }
-}
-```
-
-### Key Code Snippet 4: Two-Note GPS Flow
-
-The firmware uses a non-blocking GPS design: the initial alert Note is queued immediately so it can transmit without waiting for a fix, while GPS acquisition runs in the background without pausing fall detection or button monitoring.
-
-**Step 1 — `sendAlert()`.** Before the alert Note is sent, `loop()` computes `loc_age_s` once from a live `card.time` call and the pre-alert cache epoch — capturing the fix age at the moment the event fired. This value is passed directly to `sendAlert()` and stored in the retry-queue entry so every subsequent retry reports the same original fix age, not an age relative to the retry timestamp. The alert Note itself is queued with the Notecard's cached location embedded via `_lat`/`_lon`, `sync:true`, and the same `event_id` on every attempt. `beginGpsSearch()` is called only after `sendAlert()` returns `true`, confirming the Note was queued. This ordering guarantees no GPS search is ever orphaned by a failed `note.add`: if `sendAlert()` fails, `enqueueAlert()` adds the alert (including its `event_id` and pre-computed `loc_age_s`) to the non-blocking retry queue, and `beginGpsSearch()` fires automatically once the retry succeeds inside `pollAlertRetry()`.
-
-**Step 2 — `pollGpsSearch()`.** Called once per outer loop pass. Throttles `card.location` polls to once per 2 seconds (GNSS fixes update far slower than the outer loop rate). Accepts only a fix whose epoch post-dates the pre-alert cache snapshot to prevent a stale cached fix from being mistaken for a new acquisition. On a fresh fix, queues `beacon_location.qo` with `sync:true` and the same `event_id` as the initial alert; downstream dispatch joins the two Notes using `(device, event_id)` as the key.
-
-```cpp
-// Step 1: compute loc_age_s once at event-fire time, then assign event_id and
-// queue the alert; start GPS only after the alert is confirmed queued.
-// Capturing loc_age_s here (not inside sendAlert()) ensures every retry
-// reports the original fix age, not the age relative to the retry timestamp.
-float thisLocAgeS = /* card.time − thisCacheEpoch, or −1.0 if no fix */ ...;
-uint32_t thisEventId = ++g_alertEventId; // monotonic; incremented once per new alert
-bool sent = sendAlert(alertType, thisEventId, thisLocAgeS); // cached loc + event_id
-if (sent) {
- g_lastAlertMs = now;
- beginGpsSearch(alertType, thisEventId); // enables continuous GPS; returns immediately
-} else {
- // Preserve event-time locAgeS in the queue so all retries report the
- // original fix age — not the (growing) age at retry time.
- enqueueAlert(alertType, thisCacheEpoch, thisEventId, thisLocAgeS);
-}
-
-// Step 2: background fix acquisition (called from loop() at the outer ~10 Hz cadence)
-void pollGpsSearch() {
- if (!g_gpsSearching) return;
- // Timeout: compare elapsed time, not absolute deadline (wraparound-safe)
- if ((millis() - g_gpsSearchStart) >= (DEFAULT_GPS_TIMEOUT_SEC * 1000UL)) {
- // disable GPS; initial alert location stands
- disableGps(); return;
- }
- if ((millis() - g_gpsLastPollMs) < 2000UL) return; // throttle polls
- g_gpsLastPollMs = millis();
-
- J *rsp = notecard.requestAndResponse(notecard.newRequest("card.location"));
- uint32_t fixTime = (uint32_t)JGetNumber(rsp, "time");
- // ...
- if (fixTime > g_gpsCacheEpoch && (lat != 0.0 || lon != 0.0)) {
- // Fresh fix: queue follow-up note with event-time coordinates + same event_id
- J *req = notecard.newRequest("note.add");
- JAddStringToObject(req, "file", "beacon_location.qo");
- JAddBoolToObject(req, "sync", true);
- J *body = JAddObjectToObject(req, "body");
- JAddStringToObject(body, "type", g_gpsAlertType);
- JAddStringToObject(body, "worker_id", g_workerId);
- JAddNumberToObject(body, "event_id", (double)g_gpsEventId); // matches beacon_alert.qo
- // _lat/_lon embedded automatically by compact template at note.add time
- notecard.requestAndResponse(req);
- disableGps();
- }
-}
-```
-
-If GPS times out, only the initial `beacon_alert.qo` Note is sent; the cached location (which may be GNSS-derived, cell-derived, stale, or empty) stands. The `loc_age_s` field in the initial alert records how old the cached fix was at alert time, letting dispatch judge location freshness independently.
-
-## 8. Data Flow
-
-
-
-**Collected.** On every outer loop pass (~10 Hz): ten LIS3DH accelerometer samples read at 10 milliseconds intervals (100 Hz effective), with the two-stage fall-detection state machine evaluated on each. On each alert event: battery voltage (`card.voltage`), cached-fix age at alert time (`loc_age_s`), and a monotonic `event_id`. GPS search runs non-blocking in the background after an alert; a fresh fix, if acquired, produces a follow-up Note carrying the same `event_id`.
-
-**Location accuracy.** When an alert fires, `beacon_alert.qo` is queued immediately with the Notecard's current cached location embedded via `_lat`/`_lon` and `loc_age_s` recording the fix age in seconds (−1 if unknown). Concurrently, a non-blocking GPS search polls `card.location` at 2-second intervals for up to 90 seconds. If a fix whose epoch post-dates the pre-alert cache snapshot is acquired, a `beacon_location.qo` Note is queued with the event-time coordinates. If GPS times out, only the initial alert Note is sent and the cached location (which may be GNSS-derived, cell-derived, stale, or empty) stands.
-
-**Transmitted.**
-- `beacon_alert.qo` — emitted on confirmed fall or panic. `sync:true` triggers immediate cellular (or satellite) transmission. Contains: alert type, worker ID, `event_id` (monotonic correlation key), battery voltage, `loc_age_s` (cached-fix age at alert time), and cached location via compact template `_lat`/`_lon`.
-- `beacon_location.qo` — emitted after a confirmed alert when a fresh GPS fix arrives within the timeout window. `sync:true`. Contains: alert type (echoes the triggering event), worker ID, `event_id` (matches the paired `beacon_alert.qo`), and fresh event-time coordinates via `_lat`/`_lon`. This Note is optional — it is queued only if GPS succeeds during the background search window.
-
-**Routed.** Both Notefiles arrive at Notehub regardless of whether they came via WiFi, cellular, or Skylo satellite — the transport is transparent in the event structure. Downstream systems should pair `beacon_alert.qo` and `beacon_location.qo` using `(device, event_id)` as the join key — `event_id` is device-local, resets to 0 on each power cycle, and is not unique across devices on its own. The initial alert carries the best-available cached location, and a subsequent `beacon_location.qo` with the same `event_id` (from the same device) supersedes it with event-time coordinates when GPS acquires within the 90-second background window. Pairing on `(device, event_id)` is unambiguous across repeated alert types, retries, and network reordering.
-
-**Alert triggers:**
-- `fall` — two-stage algorithm: free-fall phase followed by impact within the detection window. Suppressed if within 60 seconds of the previous alert.
-- `panic` — panic button held for `panic_hold_ms` (default 2 seconds). Suppressed if within 60 seconds of the previous alert.
-
-## 9. Validation and Testing
-
-**Expected steady-state behavior.** In normal operation, a healthy beacon produces zero `beacon_alert.qo` events and zero `beacon_location.qo` events. To verify that Notecard provisioning, template registration, and connectivity are all working after assembly, trigger a test fall or panic (see below) and confirm the event appears in Notehub within session-establishment time (typically 30–90 seconds on cellular). If no event appears, check the Notecard's sync status via the [Blues In-Browser Terminal](https://dev.blues.io/terminal/) (`{"req":"hub.status"}`) to see the last sync time, pending Note count, and transport-layer error.
-
-**Simulating a fall.** On the bench, a realistic fall simulation: hold the device at chest height and drop it onto a padded surface from 50–80 cm. The firmware's default thresholds (0.55g free-fall, 2.5g impact) are tuned for human-body-scale falls. You should see the haptic motor pulse twice (non-blocking, monitoring continues during the buzz sequence) and a `beacon_alert.qo` Note with `"type":"fall"` appear in Notehub within session-establishment time, typically 30–90 seconds in cellular conditions. If the device acquires a fresh GPS fix during the background search window, a follow-up `beacon_location.qo` Note will appear shortly after with the event-time coordinates. In poor GNSS conditions (indoors, obstructed sky view) the background search times out after 90 seconds and only the initial alert Note is sent.
-
-**Simulating a panic.** Hold the button for 2+ seconds. After the 30 milliseconds debounce settles on the press edge, the haptic motor emits one click to confirm the press was registered. When the hold threshold is reached, the firmware evaluates the 60-second alert cooldown before queuing anything: if the cooldown has expired, the panic alert is accepted and three haptic buzzes (non-blocking, each fires 220 milliseconds apart without pausing button monitoring) confirm the alert is queued for transmission. The triple-buzz means the alert has been accepted for transmission handling — either directly into the Notecard's outbound queue (if `note.add` succeeded) or into the firmware's local retry queue for delivery as soon as the Notecard is reachable (if `note.add` failed transiently). If the device is still within the cooldown window, a single buzz acknowledges the hold without queuing an alert — use Notehub's event log to distinguish a suppressed panic from a queued one. A `beacon_alert.qo` with `"type":"panic"` should arrive in Notehub within session-establishment time. If GPS acquires a fresh fix during the background search, a `beacon_location.qo` follow-up Note will appear as well.
-
-**Simulating satellite failover.** With the device outdoors (the `MAIN` antenna has sky view), force a satellite session by temporarily restricting Notecard for Skylo to NTN-only (via `{"req":"card.transport","method":"ntn"}` issued from the blues.dev In-Browser Terminal). Trigger a panic. The alert should arrive in Notehub over the satellite path — verifiable in the event metadata, which will show `"transport":"ntn"`. Reset transport afterward to restore automatic failover: `{"req":"card.transport","method":"wifi-cell-ntn"}`.
-
-**Power validation with Mojo.** The [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) sits inline between the LiPo and the Notecarrier CX `+VBAT` pad. It accumulates the charge consumed by the board stack and makes the reading available over its Qwiic I²C link. **The beacon firmware does not read the Mojo** — it is a bench measurement instrument only. To retrieve the mAh reading during validation, connect a separate Qwiic-capable host (a SparkFun RedBoard Qwiic, a Notecarrier AL with its own sketch, or any I²C host with a Qwiic port) running the Mojo readout sketch to the Mojo's Qwiic connector. Run that host alongside the beacon during your validation window; its USB serial output gives you real-time mAh accumulation without touching the beacon firmware.
-
-**Battery runtime estimate (1200 mAh LiPo, based on measured and published figures):**
-
-Assuming steady-state idle (no alerts) and daily cellular sync:
-- Steady-state draw: ~10–20 mA (host + Notecard idle)
-- Daily sync duration: ~2 minutes (session setup + Note transmission)
-- Daily sync energy: ~0.28 mAh (Notecard sync cost)
-- **Idle energy per 24h: ~240–480 mAh (steady-state) + ~0.28 mAh (sync) = ~240–480 mAh/day**
-- **1200 mAh battery: ~2.5–5 days on steady-state + 1 sync/day, with zero alerts**
-
-Alert overhead (per event):
-- GPS acquisition (if outdoors, 90 seconds typical): +30–50 mA × 1.5 minutes = ~0.75–1.25 mAh per alert
-- Cellular sync for alert Note: ~0.28 mAh
-- **Total per alert: ~1–1.5 mAh**
-- At one alert per day: ~2.5–5 days total
-
-**Validation with Mojo is mandatory before deployment** — measure your actual steady-state draw and per-alert overhead. WiFi and satellite transports consume different energy profiles; sync cadence and GPS timeout settings affect total runtime.
-
-**Published Notecard for Skylo figures (from Blues documentation)**
-
-| Component | State | Published figure |
-|---|---|---|
-| Notecard for Skylo (NOTE-NBGLWX) | Idle, radio off (between sessions) | ~8 µA ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)) |
-| Notecard for Skylo (NOTE-NBGLWX) | Network session (cellular or satellite) | ~250 mA average from the onboard BG95-S5 modem; brief peaks up to ~2 A for a few ms on a 2G transmit burst ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)) |
-
-**Estimated whole-device figures — validate with Mojo before sizing for deployment**
-
-| State | Estimated draw | Notes |
-|---|---|---|
-| Host awake, Notecard idle (steady-state background) | ~10–20 mA | STM32L433 at default clock; not from Blues datasheet — disable debug Serial to reduce clock load |
-| GNSS acquisition active (up to 90 seconds per alert event) | +30–50 mA above baseline | Notecard GNSS module draw; validate with Mojo |
-| Haptic motor active (ERM disc, ~0.5 seconds per buzz) | +50–80 mA above baseline | DRV2605L + motor; validate with Mojo |
-| Alert event (GPS acquisition + cellular sync) | ~40–70 mA for up to 90 seconds | Dominant transient; rare in normal operation; validate with Mojo |
-
-A productive bench exercise: run the device for 2 hours on a known-capacity LiPo and Note the Mojo's mAh reading. Trigger several test falls and panics and compare the per-sync energy spikes against the alert count. Steady-state draw between alerts should stay close to the host-idle estimate (~10–20 mA). If steady-state draw is well above 20 mA, the host MCU is likely running at full clock — consider disabling debug serial output, which forces the STM32 to maintain its USB clock. Use Mojo-measured average current, not the steady-state estimate — to project battery runtime for your specific alert frequency and transport conditions.
-
-## 10. Troubleshooting
-
-**Device does not appear in Notehub.**
-- Verify the `PRODUCT_UID` in `lone_worker_beacon_helpers.h` matches your Notehub project's ProductUID exactly.
-- Check that the Notecard has a provisioned SIM (should ship with one; confirm at [Blues shop](https://shop.blues.com)).
-- Ensure cellular or WiFi coverage is available. If indoors and no WiFi is provisioned, power cycle and move to a window or open area.
-- From the [Blues In-Browser Terminal](https://dev.blues.io/terminal/), issue `{"req":"hub.status"}`. If `last_sync` is very recent, the Notecard is communicating. If `last_sync` is old or `status` is `error`, the Notecard cannot reach cellular or Notehub.
-
-**Panic button or fall detection does not trigger.**
-- Check the haptic motor for signs of life: power on the beacon, wait 5 seconds for boot to complete, then hold the panic button for 3+ seconds. You should feel a vibration within 1–2 seconds of pressing. If no vibration, check the motor's wiring to the DRV2605L and that the DRV2605L is seated on I²C (address 0x5A).
-- Verify the LIS3DH is responding: after boot, if you see no double-buzz (fault pattern), the accelerometer initialized. Try triggering a fall: drop the beacon 50–80 cm onto a pillow or padded surface. If still no double-buzz on alert, check the LIS3DH's I²C wiring (SDA, SCL, 0x18 address) and that the SDO pin is grounded or left floating.
-- Check the startup fault pattern: a slow double-buzz (buzz, pause, buzz) repeating every 5 seconds indicates a boot-time configuration error (missing `PRODUCT_UID`, failed `hub.set`, or template registration failure). Verify the `PRODUCT_UID` and power-cycle the beacon.
-
-**Alerts appear in Notehub but coordinates are missing or stale.**
-- `loc_age_s: -1.0` means the Notecard had no cached GPS fix when the alert fired. This is normal indoors. If outdoors with a clear sky view and `loc_age_s` is still -1 after multiple alerts, the Notecard may not have acquired a GPS fix since power-on. Run `{"req":"card.location"}` from the [Blues In-Browser Terminal](https://dev.blues.io/terminal/) to force a fix attempt and check the response.
-- No `beacon_location.qo` follow-up Note means GPS did not acquire a fresh fix within the 90-second background search window. This is normal indoors or under heavy tree cover. If GPS should be working, trigger a test alert and check the Notecard's `card.location` response for lock time and signal quality.
-
-**Battery drains too quickly.**
-- Verify the host MCU is not stuck in a high-clock state. From the Arduino IDE Serial Monitor (115200 baud, after uncommenting `#define DEBUG_SERIAL` in the .ino file), you should see periodic log messages (one per outer loop pass, ~10 per second) with healthy current draws. If the Serial Monitor is active and you see frequent output, the debug serial is forcing the STM32 to maintain its USB clock — disable `DEBUG_SERIAL` to reduce MCU idle from ~10–15 mA to ~5–10 mA.
-- Confirm the Notecard is in low-power periodic mode: issue `{"req":"hub.get"}` from the [Blues In-Browser Terminal](https://dev.blues.io/terminal/). If `outbound` is greater than 1440 (24 hours) or `inbound` is less than 120 (2 hours), sync cadence may be excessive. See [Section 5, step 5](#6-notehub-setup) for recommended settings.
-- Use Mojo (see [Section 8](#9-validation-and-testing)) to measure actual current in different states (idle, fall/panic event, GPS search, sync). Compare against the estimated figures in the power validation table. If measured idle is >25 mA, a peripheral may be drawing unexpectedly — check I²C for clock-stretching issues or peripheral misconfigurations.
-
-**Skylo satellite transmission fails or never completes.**
-- The Skylo NTN path is only active after Notecard for Skylo has completed at least one successful cellular or WiFi sync to associate with your project and deliver the compact template definitions to Notehub. If the beacon launches in a no-cellular area, it cannot use satellite until it has found cellular (or WiFi) coverage at least once. Move the device to coverage, power it on, wait for a sync (check Notehub device status), then move back to the satellite-only zone.
-- Verify the `MAIN` antenna orientation: it should face the sky (polycarbonate enclosure top is sufficient); in the northern hemisphere, southward orientation improves link margin. If the antenna is buried against a body or oriented away from the sky, Skylo acquisition may fail.
-- Check Skylo coverage: the Skylo network covers specific regions (see the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)). If you're outside the coverage footprint, NTN transmission will fail silently.
-- From the [Blues In-Browser Terminal](https://dev.blues.io/terminal/), issue `{"req":"card.transport","method":"ntn"}` to force satellite-only operation (cellular and WiFi disabled). Trigger a test panic from a location with sky view. Check the event metadata: if `"transport":"ntn"` is present, the satellite path is working. Re-enable automatic failover: `{"req":"card.transport","method":"wifi-cell-ntn"}`.
-
-**Fall detection generates false positives during normal work.**
-- Adjust the `freefall_g` and `impact_g` thresholds via environment variables (see [Section 5, step 5](#6-notehub-setup)). Increase `freefall_g` (e.g., 0.65 or 0.75) to require a deeper free-fall phase before the impact window opens. Increase `impact_g` (e.g., 3.0 or 3.5) to require a larger acceleration spike to confirm impact. Both changes reduce sensitivity and may suppress legitimate falls — validate with your specific worker activity profile.
-- Shorten the `fall_window_ms` (e.g., 300 milliseconds instead of 500 milliseconds) to close the impact window sooner, requiring impact to occur more tightly coupled to the free-fall phase. This rejects impact spikes that occur seconds after a bump.
-- Run a 24-hour learning period with each worker activity profile and log the Notecard's accelerometer telemetry (see README Section 6). Identify the baseline g-profile of normal work (walking, climbing, tool swings) and set thresholds to sit just above the highest "false positive" peak observed during normal use.
-
-**Multiple alerts fire from a single fall.**
-- The 60-second alert cooldown (`DEFAULT_ALERT_COOLDOWN_SEC`) prevents alert storms. A second fall detected within 60 seconds of the previous alert is suppressed — the button feels one buzz (acknowledgment of the press) instead of three (alert accepted). This is intentional. If you need multiple alerts for a tumbling device, increase the cooldown in the firmware or set it via an environment variable (when supported in a future revision).
-
-## 11. Limitations and Next Steps
-
-This is a reference design, not a finished safety product. Several things a real lone-worker fleet would demand have been left for a production team to add — most notably a certified safety claim and a sleeping host MCU — and they are listed below as scope choices, not surprises. The forward-looking work that turns this into a deployable wearable follows the limitations.
-
-### Simplified for the POC
-
-The simplifications below are scope choices, not surprises — each names something a real lone-worker fleet would add before deployment.
-
-**Not a certified life-safety device.** This proof-of-concept has not been evaluated for fail-safe operation or certified under any personal-emergency-response or lone-worker protection standard. Validate all alert behaviors against your own safety requirements, and treat this design as a starting point rather than a production safety system. Supplement it with — **do not use it to replace** — mandatory check-in procedures, required PPE, and any regulated safety systems that apply to your operation.
-
-**Host MCU never sleeps.** The firmware runs a dual-cadence polling loop: the LIS3DH is sampled at ~100 Hz via a 10-sample inner loop (10 milliseconds between reads), and Notecard/GPS/haptic state advances at the outer ~10 Hz cadence. A production implementation should use STM32L433 STOP2 mode with GPIO interrupt wakeup from the LIS3DH INT1 pin and the panic button, dropping host idle draw from ~10+ mA to ~2–3 µA. The LIS3DH's hardware free-fall interrupt handles Stage 1 detection in silicon without host MCU involvement — the host only wakes when the interrupt fires. **This is the single biggest power optimization available and the most important production change.**
-
-**Fall detection is software-sampled.** The LIS3DH is sampled at ~100 Hz via the inner loop (10 reads per outer pass, 10 milliseconds apart), matching the sensor ODR and reliably catching free-fall phases as short as 80 ms. The remaining limitation is that Stage 1 (free-fall detection) is implemented in host firmware rather than the LIS3DH hardware interrupt registers, so the host MCU must stay awake continuously. Using the LIS3DH INT1 free-fall interrupt (via `INT1_CFG`, `INT1_THS`, `INT1_DURATION` registers) is the production upgrade: it offloads Stage 1 entirely to the accelerometer silicon, allows the STM32L433 to sleep in STOP2 between events, and eliminates the polling overhead.
-
-**Fall detection thresholds are heuristic.** The default 0.55g free-fall / 2.5g impact thresholds cover textbook falls from standing height onto hard surfaces. They will produce false negatives for soft-surface landings (carpeted floors, mud) where the impact spike is attenuated, and may produce false positives during vigorous physical work involving overhead tool swings. Production deployments should run a calibration period with each worker activity profile before enabling real-time dispatch.
-
-**No cancel flow after panic.** The current firmware has no mechanism for a worker to cancel a panic alert once it's been confirmed and sent. A production device should include a multi-step cancel: button press within 60 seconds of a panic, haptic confirmation, and a `cancel` Note that the dispatch system can act on.
-
-**GPS follow-up Note is optional and serialized.** When a fall or panic alert fires, the initial `beacon_alert.qo` Note is queued immediately with the Notecard's cached location. The background GPS search then runs for up to `DEFAULT_GPS_TIMEOUT_SEC` (90 seconds) without blocking the detection loop. If a fresh fix arrives, a `beacon_location.qo` Note is queued with the event-time coordinates and the same `event_id`. If GPS times out — because the device is indoors, under heavy tree cover, or the Notecard has no sky view — only the initial alert Note is delivered and its cached location (which may be stale or empty) stands. Dispatch should treat a missing `beacon_location.qo` (matching `event_id`) as an indication that the event-time position is unknown, **not** that the alert failed. Only one GPS enrichment window can run at a time — a second alert that fires during the 90-second window (the 60-second cooldown is shorter than the GPS timeout) receives its cached location only and does not get a `beacon_location.qo` follow-up. A production system that requires fresh GPS for every alert should serialize the alert cadence (e.g., extend the cooldown to match the GPS timeout) or implement a per-alert GPS job queue.
-
-**No data encryption.** Notes travel over TLS between Notecard and Notehub; the Notefile body is not additionally encrypted. For sensitive safety applications with worker location data, consider using `beacon_alert.qos` (`.qos` suffix enables encrypted transport at the Notecard level).
-
-**Skylo NTN requires an initial non-NTN sync.** The Skylo satellite (NTN) path is not available until Notecard for Skylo has completed at least one successful cellular or WiFi session to associate with Notehub and register the Notefile templates. A device that ships directly into a no-cellular zone will be unable to send via satellite until it has found cellular (or WiFi) coverage at least once. Pre-provisioning during QA on a cellular-capable bench is the standard mitigation.
-
-**Satellite payload budget.** The Notecard for Skylo bundle includes 10 KB of Skylo satellite data, and the Skylo NTN link enforces a hard 256-byte maximum per Note. The compact alert template is well under that ceiling; frequent triggering in a no-cellular environment will consume the bundle faster.
-
-**Single I²C bus for all peripherals.** The LIS3DH and DRV2605L share the bus with the internal Notecard connection. A severe I²C lockup (e.g., a partially-completed transaction interrupted by a reset) could block all communication. A production design should include bus-error recovery and a hardware watchdog.
-
-**No charging subsystem.** This POC documents a bare LiPo cell; no charger, dock, cradle, or inductive charging coil is included in the BOM or wiring. A production wearable needs an appropriate charging path, for example, a USB-C LiPo charging circuit integrated into the enclosure — plus overcharge and short-circuit protection if not already provided by the cell's built-in circuitry. Rechargeable operation is a natural next step but is out of scope for the POC.
-
-**Mojo is bench-only in this POC.** The firmware does not read the Mojo's charge accumulation register over Qwiic — the Mojo is a bench measurement instrument only. A production extension could include a `mah_consumed` field in `beacon_alert.qo` for fleet-level battery-health monitoring, read via the Mojo's Qwiic I²C link.
-
-### Production Next Steps
-
-The forward-looking work that turns this into a deployable wearable follows, roughly from the most impactful power and safety changes to per-worker refinements.
-
-**STM32L433 STOP2 low-power mode** with LIS3DH INT1 hardware interrupt wakeup is the single most impactful power optimization, dropping host idle from ~10–15 mA to ~2–3 µA.
-
-**LIS3DH hardware free-fall and shock detection** configured via the `INT1_CFG`, `INT1_THS`, and `INT1_DURATION` registers offloads Stage 1 detection entirely to the accelerometer silicon.
-
-**A cancel-alert flow** gives the worker a way out of a false alarm: a post-panic confirmation cancel within N seconds, routed as a `cancel` event on `beacon_alert.qo` carrying the same `event_id` as the alert being canceled.
-
-**Worker check-in acknowledgment** closes the loop back to the worker: a dispatcher can send a Notehub [Signal](https://dev.blues.io/api-reference/glossary/#signal) back to the device, triggering a distinctive haptic pattern so the worker knows their alert was received.
-
-**Field-upgradeable firmware** via [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) lets threshold recipes be pushed to the whole fleet without a physical re-flash.
-
-**A `.qos` encrypted Notefile** protects worker location data in privacy-sensitive jurisdictions.
-
-**Per-worker baseline calibration** records each worker's typical activity vibration profile via a 24-hour learning period, then tunes `impact_g` and `freefall_g` individually.
-
-## 12. Summary
-
-The lineman at the edge of the substation, the pumper at the rural wellhead, the field tech in the 2 AM boiler room — each of them now clips on a device that does what no check-in procedure ever could: it watches them automatically, with no worker action required, and reaches a dispatcher even when cellular goes dark. The two-stage fall algorithm rejects everyday bumps without losing genuine falls; the panic button is there for the situations that don't look like physics; Notecard for Skylo's onboard satellite radio covers the specific sites where cellular fails first and matters most — no companion module, no second device. The cellular path handles the vast majority of activations quickly and inexpensively; the satellite path is the safety margin underneath it. That combination, in a belt-clip enclosure, is the practical shape of lone-worker safety assurance — supplementing, not replacing, the procedures and PPE that came before it.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/lone-worker-panic-and-fall-detection-beacon/)**
diff --git a/60-lone-worker-panic-fall-detection-beacon/banner.png b/60-lone-worker-panic-fall-detection-beacon/banner.png
deleted file mode 100644
index 0acb73fd..00000000
Binary files a/60-lone-worker-panic-fall-detection-beacon/banner.png and /dev/null differ
diff --git a/60-lone-worker-panic-fall-detection-beacon/diagrams/01-system-architecture.svg b/60-lone-worker-panic-fall-detection-beacon/diagrams/01-system-architecture.svg
deleted file mode 100644
index 8a1206ac..00000000
--- a/60-lone-worker-panic-fall-detection-beacon/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-
diff --git a/60-lone-worker-panic-fall-detection-beacon/diagrams/02-wiring-assembly.svg b/60-lone-worker-panic-fall-detection-beacon/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 1cfe0bd9..00000000
--- a/60-lone-worker-panic-fall-detection-beacon/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,92 +0,0 @@
-
diff --git a/60-lone-worker-panic-fall-detection-beacon/diagrams/03-data-flow.svg b/60-lone-worker-panic-fall-detection-beacon/diagrams/03-data-flow.svg
deleted file mode 100644
index 47406c03..00000000
--- a/60-lone-worker-panic-fall-detection-beacon/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,67 +0,0 @@
-
diff --git a/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon.ino b/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon.ino
index 7bf21f29..708266ca 100644
--- a/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon.ino
+++ b/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon.ino
@@ -55,9 +55,10 @@
*
* Dependencies:
* Blues Wireless Notecard (note-arduino)
- * SparkFun LIS3DH Arduino Library v1.0.3+
+ * Adafruit LIS3DH Library v1.3.0+
* Adafruit DRV2605 Library v1.2.3+
- * Adafruit BusIO (DRV2605 dependency) v1.15+
+ * Adafruit Unified Sensor (LIS3DH dep) v1.1.14+
+ * Adafruit BusIO (DRV2605/LIS3DH dep) v1.15+
*/
#include
@@ -70,7 +71,7 @@
// ── Objects ───────────────────────────────────────────────────────────────
Notecard notecard;
-LIS3DH accel(I2C_MODE, 0x18); // SDO pulled low → address 0x18
+Adafruit_LIS3DH accel; // I2C address passed to begin() in initAccel()
Adafruit_DRV2605 haptic;
// ── Runtime config (shadows env vars; refreshed every ENV_FETCH_INTERVAL_MS)
diff --git a/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon_helpers.cpp b/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon_helpers.cpp
index a416b561..a8b667d5 100644
--- a/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon_helpers.cpp
+++ b/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon_helpers.cpp
@@ -231,14 +231,26 @@ void fetchEnvVars()
// ─── Sensor Initialization ────────────────────────────────────────────────
bool initAccel()
{
- accel.settings.adcEnabled = 0;
- accel.settings.tempEnabled = 0;
- accel.settings.accelSampleRate = 100; // ODR 100 Hz; matches ACCEL_SAMPLE_MS
- accel.settings.accelRange = 4; // ±4 g; headroom for impacts
- accel.settings.xAccelEnabled = 1;
- accel.settings.yAccelEnabled = 1;
- accel.settings.zAccelEnabled = 1;
- return (accel.begin() == IMU_SUCCESS);
+ // Adafruit_LIS3DH::begin() probes WHO_AM_I (returns false if the part does
+ // not answer on ACCEL_I2C_ADDR), enables all three axes, and leaves the
+ // device in high-resolution 12-bit mode with BDU set so multi-byte reads
+ // are coherent. It does NOT default to the rate or range this project
+ // needs — 400 Hz and ±2 g — so both are set explicitly below.
+ if (!accel.begin(ACCEL_I2C_ADDR)) return false;
+
+ accel.setRange(LIS3DH_RANGE_4_G); // ±4 g; headroom for impacts
+ accel.setDataRate(LIS3DH_DATARATE_100_HZ); // ODR 100 Hz; matches ACCEL_SAMPLE_MS
+
+ // begin() also writes TEMPCFG = 0x80, which powers up the auxiliary ADC.
+ // Neither the ADC inputs nor the temperature sensor are used here and this
+ // is a battery-powered beacon, so clear the register to switch both off —
+ // the equivalent of adcEnabled/tempEnabled = 0 under the previous driver.
+ // (begin() additionally routes DRDY to INT1; that pin is unconnected in
+ // this build, so the extra interrupt line is harmless.)
+ Wire.beginTransmission(ACCEL_I2C_ADDR);
+ Wire.write(LIS3DH_REG_TEMPCFG);
+ Wire.write(0x00);
+ return (Wire.endTransmission() == 0);
}
bool initHaptic()
@@ -261,9 +273,10 @@ bool initHaptic()
// Impact window uses start-time + elapsed comparison (wraparound-safe).
bool pollFallDetection()
{
- float ax = accel.readFloatAccelX();
- float ay = accel.readFloatAccelY();
- float az = accel.readFloatAccelZ();
+ accel.read(); // refreshes x_g / y_g / z_g, already in g
+ float ax = accel.x_g;
+ float ay = accel.y_g;
+ float az = accel.z_g;
float totalG = sqrtf(ax*ax + ay*ay + az*az);
uint32_t now = millis();
@@ -271,6 +284,13 @@ bool pollFallDetection()
// All-zero vector: LIS3DH typically measures ~1 g at rest due to gravity;
// an all-zero result indicates an I2C fault or sensor hang.
// totalG > ACCEL_PLAUSIBLE_G_MAX: physically impossible on a ±4 g device.
+ //
+ // Note: Adafruit_LIS3DH::read() returns void and ignores the underlying
+ // bus status, so a failed I2C transfer leaves whatever was in its local
+ // buffer rather than reliably zeroing the axes. The magnitude bound below
+ // catches most such reads, and any that slip through are caught by
+ // tryReinitAccel(), which re-probes WHO_AM_I via begin(). Do not treat a
+ // plausible-looking sample as proof the bus is healthy.
bool readingBad = (fabsf(ax) < 0.001f && fabsf(ay) < 0.001f &&
fabsf(az) < 0.001f) || (totalG > ACCEL_PLAUSIBLE_G_MAX);
if (readingBad) {
diff --git a/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon_helpers.h b/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon_helpers.h
index e2b058b3..5b1ee9c0 100644
--- a/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon_helpers.h
+++ b/60-lone-worker-panic-fall-detection-beacon/firmware/lone_worker_beacon/lone_worker_beacon_helpers.h
@@ -20,7 +20,7 @@
#include
#include
-#include
+#include
#include
#include
#include
@@ -112,10 +112,13 @@ struct AlertQueueEntry {
// (80 ms) is reliably observed regardless of the outer loop's ~10 Hz cadence.
#define ACCEL_SAMPLES_PER_LOOP 10 // inner samples per outer iteration
#define ACCEL_SAMPLE_MS 10 // ms between inner samples (LIS3DH at 100 Hz ODR)
+#define ACCEL_I2C_ADDR 0x18 // SDO/SA0 pulled low → 0x18 (0x19 if high)
-// Runtime health: if totalG is near-zero (all-axis zero = I2C fault) or
-// implausibly high (above the ±4 g full-scale range plus headroom), the read
-// is considered bad. ACCEL_FAIL_THRESHOLD consecutive bad reads trigger a
+// Runtime health: if totalG is near-zero (all axes reading zero, which a
+// working LIS3DH will not do at rest under gravity) or implausibly high (above
+// the ±4 g full-scale range plus headroom), the read is considered bad. See the
+// note in pollFallDetection() on why a plausible sample is not proof of a
+// healthy I2C bus. ACCEL_FAIL_THRESHOLD consecutive bad reads trigger a
// reinitialization attempt; after ACCEL_REINIT_MAX failed reinits the fault
// is latched and fall detection is disabled permanently until power-cycle.
#define ACCEL_FAIL_THRESHOLD 5 // consecutive bad reads before reinit
@@ -154,7 +157,7 @@ struct AlertQueueEntry {
// ── Shared objects (defined in lone_worker_beacon.ino) ────────────────────
extern Notecard notecard;
-extern LIS3DH accel;
+extern Adafruit_LIS3DH accel;
extern Adafruit_DRV2605 haptic;
// ── Runtime config (defined in lone_worker_beacon.ino) ────────────────────
diff --git a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/README.md b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/README.md
index f8bf5912..2dcf64f1 100644
--- a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/README.md
+++ b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/README.md
@@ -1,529 +1,5 @@
-# Pharmacy/Lab Cold-Storage Audit Monitor
+# Regulatory-Grade Pharmacy and Lab Cold Storage Audit Monitor
-
+A cellular reference design providing continuous, timestamped temperature records for pharmacy and lab cold storage, with immediate alerts on out-of-range conditions.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Pharmacy%2FLab%20Cold-Storage%20Audit%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/cold_storage_audit_monitor/](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](https://blues.com/safety-assurance/) reference design that gives pharmacies, clinical laboratories, and vaccine depots a continuous, automatically-timestamped temperature record for every refrigerator and freezer in their compliance scope, delivered over a cellular data path that bypasses the facility's regulated network entirely. Immediate alerts fire the moment the temperature strays outside its configured range or a door is left open too long.
-
-## 1. Project Overview
-
-**The problem.** Pharmacies, clinical laboratories, and vaccine depots are subject to a patchwork of overlapping regulations — USP Chapter 659 (Packaging and Storage Requirements), FDA 21 CFR Part 211.68, state board-of-pharmacy rules, and for federally funded programs, CDC Vaccine Storage and Handling guidelines. Every one of those frameworks requires automated temperature records with defined excursion thresholds: 2°C–8°C for most refrigerated vaccines and biologics, with documentation of any deviation, its duration, and the corrective action taken.
-
-The problem is not that facilities lack refrigerators — it's that most of them lack automated monitoring. A datalogger that must be manually downloaded, a wall thermometer read once per shift, or a WiFi-connected sensor that goes dark whenever the facility's network hiccups are all insufficient 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 is complete even when nobody was watching.
-
-This reference design demonstrates how to close that gap. The Notecarrier CX, with its onboard Cygnet STM32 host, wakes every five minutes to read a high-precision digital temperature sensor, check a magnetic door switch, and correlate both readings against an ambient-light sensor inside the unit. Each wake produces one timestamped reading Note queued in the Notecard's on-device store; the queue flushes to the [Blues Notehub](https://blues.com/notehub/) cloud service on the scheduled cellular connection. If the temperature strays outside configured limits, or if the door is left open beyond an acceptable threshold, an alert Note is transmitted immediately — bypassing the batched outbound window and landing in whatever on-call or compliance system the operator routes it to.
-
-**Why Notecard.** Pharmacies and clinical labs operate tightly managed network environments. PCI-compliant retail pharmacy networks, HIPAA-covered clinical networks, and federally regulated vaccine storage programs all share one thing: strict rules against unknown IoT-class devices on the primary LAN. A temperature sensor attached to the pharmacy WiFi is either going to fail a network security review or be quietly firewalled off from the internet. An independent cellular data path sidesteps that entire conversation — the Notecard registers on the carrier's network directly, never touches the facility's LAN, and the IT team never has to issue a network access request or sign a BAA for a refrigerator.
-
-
-
-That independence also matters for continuity. Facility WiFi goes down for maintenance, for storms, for power events — sometimes for hours. A WiFi-dependent compliance monitor is exactly the kind of device that silently stops logging at the worst possible moment. The Notecard's store-and-forward queue buffers Notes locally through any connectivity gap and syncs them when the cellular session resumes, with timestamps intact. The audit-evidence record this design produces consists of two streams: **per-sample reading Notes** (one timestamped Note per 5-minute wake, individually queued in the Notecard's flash-backed store) and **immediate alert Notes** (emitted the moment a threshold is tripped, regardless of the scheduled sync cadence). Every individual reading is persisted as a separate Note in the Notecard's on-device flash queue — no aggregation — preserving sample lineage across cellular connectivity gaps so an auditor can reconstruct the exact temperature history and door-event timeline for any window. For an auditor reviewing a weekend excursion event, that buffered record — every individual sample plus any alert Notes that fired — is not a nice-to-have. It is the record. (This store-and-forward guarantee covers cellular and WiFi outages; a separate, shallower host-side retry ring handles the distinct case where the host cannot reach the Notecard over I²C. See [§7 Retry and error handling](#retry-and-error-handling) and [Limitations](#11-limitations-and-next-steps).)
-
-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. Using the facility's primary pharmacy or clinical LAN defeats the network-independence rationale of this design, and a compliance monitor sitting 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 door-gasket pass-through (see [§5 Wiring and Assembly](#5-wiring-and-assembly)), placing the stainless-steel probe capsule at the geometric center of the storage volume. The VEML7700 light sensor mounts on the exterior of the door frame — not inside the cold zone — where it detects light spillage when the door is ajar. See [Limitations](#11-limitations-and-next-steps) for condensation-tolerant production placement options. Door switch halves mount on the door and frame. The Notecarrier CX sits in the enclosure, USB-C powered from a wall adapter. The cellular antenna mounts on the exterior of the enclosure where it has line of sight to the network. No network configuration, no IT ticket, no manual download. **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 Cygnet STM32 host on the Notecarrier CX wakes, takes a single coordinated snapshot — MAX31865 over SPI, VEML7700 over I²C, the reed switch from a GPIO — and runs the four alert rules against that snapshot before going back to sleep. Between samples the host is fully powered off: the Notecard's ATTN pin drives the Notecarrier CX enable gate, cutting Cygnet power entirely via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) sleep mode. Anything the firmware needs to remember across that sleep — the door-open timestamp the compliance officer cares about, the per-alert cooldown timers — is serialized into the Notecard's flash before sleep and rehydrated at the next wake via the `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` helper pair from the `note-arduino` library.
-
-**Notecard responsibilities.** The Notecard does three jobs that together produce the audit-evidence record. First, it queues [Notes](https://dev.blues.io/api-reference/glossary/#note) in on-device flash and flushes them on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) outbound cadence (default 60 minutes) — but any `sync:true` alert Note bypasses that schedule and opens a session immediately. Second, it maintains a real-time clock synchronized to UTC via Notehub, supplying the epoch timestamps the firmware needs for door-duration tracking and alert cooldowns, and stamping every Note's `when` field with that 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](#11-limitations-and-next-steps).) Third, it distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-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 gets the device onto carrier cellular worldwide and delivers data to [Notehub](https://dev.blues.io/notehub/notehub-walkthrough/) over the Internet. From there Notehub stores every event and applies project-level routes. Reading Notes and alert Notes land in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile), so the long-term compliance archive and the on-call pager get exactly the volume each needs — no filtering logic in the route. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) lets a multi-site operator group units by storage class (refrigerator vs. freezer vs. ultra-cold) and push the matching threshold preset across the whole class at once.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — this project ships no 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 that sends timestamped readings to Notehub every 5 minutes, with immediate alerts on temperature excursions or prolonged door-open events. Readings accumulate 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):**
-1. Obtain a Notecarrier CX + MBGLW, VEML7700 sensor, and magnetic door switch
-2. Wire the three sensors (I²C, GPIO, and Qwiic as shown in [§5](#5-wiring-and-assembly))
-3. Clone this repo; paste your Notehub ProductUID into `firmware/cold_storage_audit_monitor/cold_storage_audit_monitor.ino` (line 51)
-4. Flash with `arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/ && arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/ttyACM0 firmware/` (adjust port for your OS, this FQBN matches `firmware/cold_storage_audit_monitor/sketch.yaml`, so omitting `-b` also works when invoked from the sketch directory)
-5. Power up; verify readings appear in Notehub within 60 seconds (may take 1–5 minutes on first power for cellular registration)
-6. Override thresholds in Notehub **Fleet → Environment** (e.g., `temp_high_alert_c: 8.0`, `temp_low_alert_c: 2.0` for refrigerated storage)
-
-**For production:** Follow §10 and obtain a NIST-calibrated PT1000 probe assembly before regulatory deployment.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "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](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an onboard Cygnet STM32 host MCU — no separate MCU needed. ATTN pin wired to the enable gate so the Notecard can cut host power during sleep. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 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](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter for bench-top current-draw validation. Inline on the +VBAT rail during development; not deployed in the enclosure. |
-| [Adafruit Platinum RTD Sensor — PT1000, 3-Wire, 1 m (Product 3984)](https://www.adafruit.com/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. Operating range −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** — a dedicated RTD probe rated to −80°C or below is required for that range and is outside the scope of this reference design. The probe capsule routes into the refrigerated compartment; the cable routes 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; keep the certificate on file with the unit's commissioning documentation. |
-| [Adafruit PT1000 RTD Temperature Sensor Amplifier — MAX31865 (Product 3648)](https://www.adafruit.com/product/3648) | 1 | **Production RTD amplifier.** Interfaces the 3-wire PT1000 probe to the Notecarrier CX via hardware SPI (CS on D10). Onboard 4300 Ω reference resistor and 3.3 V regulator; 5 V tolerant. Mounts inside the electronics enclosure. Configure the 2/3-wire solder jumper on the bottom of the board for 3-wire mode before wiring. Firmware uses `rtdAmp.begin(MAX31865_3WIRE)` and `rtdAmp.temperature(1000, 4300.0)`. |
-| [SparkFun TMP117 High Precision Temperature Sensor — Qwiic (SEN-15805)](https://www.sparkfun.com/products/15805) | 1 | **Bench substitute only, not the production sensor path.** ±0.1°C accuracy, I²C via Qwiic. Using TMP117 requires swapping the `Adafruit_MAX31865` library for `SparkFun_TMP117` and replacing `readTemperatureC()` in `firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.cpp`. Mounts inside the enclosure; measures exterior ambient air only. Does not ship with a NIST-traceable calibration certificate. Not a deployable compliance instrument. |
-| [Adafruit VEML7700 Lux Sensor — STEMMA QT / Qwiic (Product 4162)](https://www.adafruit.com/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 intermediate). Positioned near the door opening, it detects the interior lamp independently of the door switch — providing a second line of evidence for door-open events and flagging stuck-closed switch states when light and switch disagree. 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](#11-limitations-and-next-steps). |
-| [SparkFun Magnetic Contact Switch Set (COM-13247)](https://www.sparkfun.com/products/13247) | 1 | Normally-Open (NO) reed switch assembly. Magnet on the door, switch body on the frame. Mechanically simple, no power consumption, and directly connected to a digital GPIO with pull-up — no ADC or signal conditioning required. |
-| [Cellular Antenna — 698–2700 MHz, SMA (SparkFun CEL-16432)](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html) | 1 | Wideband external antenna covering the LTE Cat-1 bis frequency range used by the MBGLW across 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. Must be routed outside the enclosure — a metal insulated enclosure or the body of an adjacent refrigerator will heavily attenuate an internal antenna and can prevent 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)](https://www.adafruit.com/product/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; the Notecarrier CX has a standard 0.1" dual-row header. |
-| Qwiic cable, 500 mm (e.g. [SparkFun PRT-14429](https://www.sparkfun.com/products/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 — there is no TMP117 intermediate in the I²C chain. A longer cable may be needed depending on enclosure placement and door geometry. |
-| Qwiic cable, 100 mm (e.g. [SparkFun PRT-14427](https://www.sparkfun.com/products/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. Not installed in the deployed enclosure. |
-| Nylon cable glands, M16 or equivalent, for 5–10 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 to match the cable OD; M16 glands (available from enclosure suppliers or electronics distributors) suit typical Qwiic (~3 mm) and SMA pigtail diameters. The Adafruit PT1000 probe cable is 2.8 mm OD — an M12 gland fits as well. |
-| [USB-C Power Supply, 5.1V 3A (Adafruit 4298)](https://www.adafruit.com/product/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 including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-**Notecard installation:**
-
-1. Seat the Notecard Cell+WiFi (MBGLW) into the M.2 slot on the Notecarrier CX — the card inserts at a shallow angle and clicks flat, then the retaining screw locks it down.
-2. 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; do not confuse it with any GPS u.FL port.
-3. 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.
-
-**Enclosure location:**
-
-Mount the electronics enclosure on the **exterior** of the cold-storage unit, never inside the refrigerated compartment. Two reasons: first, a metal insulated refrigerator body or adjacent metal surfaces will heavily attenuate the cellular radio signal, which can prevent network registration entirely; second, sustained condensation inside a refrigerated metal box will eventually damage unprotected electronics. A sealed weatherproof enclosure (e.g. Hammond 1554C2BK) mounted on the exterior side 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 via 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](#rtd-temperature-amplifier-max31865)). The door switch uses only a digital GPIO and GND from the dual 16-pin header.
-
-**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 places the VEML7700 near the exterior 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 — there is no TMP117 in the I²C chain. The MAX31865 RTD amplifier connects via SPI (see [RTD temperature amplifier](#rtd-temperature-amplifier-max31865) below). Do not permanently mount the bare VEML7700 PCB inside the refrigerated compartment. See [Light sensor placement](#light-sensor-placement) and [Limitations](#11-limitations-and-next-steps).
-- VEML7700 is powered from the Qwiic 3.3 V rail; no additional power wiring needed.
-- 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).
-- Firmware enables `INPUT_PULLUP` on D5. Door closed (magnet present): reed contacts close, D5 pulled LOW. Door open (magnet removed): reed contacts open, 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 using six female-to-female jumper wires over 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 — 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](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/)). The table above gives the correct physical connections. The Arduino STM32 SPI library drives the correct hardware-peripheral lines regardless of the silkscreen; the swap only affects how you run the jumper wires.
-
-
-
-**PT1000 probe connection.** The Adafruit PT1000 probe (Product 3984) terminates 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](https://learn.adafruit.com/adafruit-max31865-rtd-pt100-amplifier/). Connect the wires to the MAX31865 screw terminal blocks as described in that guide 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](#temperature-sensor-placement) below). The 316L stainless-steel capsule at the end of the 1 m cable is rated to −50°C to 280°C and is the element that enters the cold zone.
-
-### Temperature sensor placement
-
-A probe assembly has a cable-mounted sensing element, typically a stainless-steel tip on a flexible lead — that routes from the enclosure into the refrigerated compartment through one of the following paths:
-
-- **Preferred — manufacturer probe port.** Many pharmacy-grade and laboratory refrigerators/freezers include a factory-drilled, gasketed probe port (typically a rubber plug or compression fitting in the cabinet wall) specifically for external sensor cables. Consult the equipment manual and use this port if present.
-- **Alternative — door-gasket dress.** Route the probe lead through the hinge-side corner of the door seal gasket, where the seal compresses the least and cable crush is minimal. Verify the door closes and latches normally after routing.
-
-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 whose locations are not visible from the exterior. Unauthorized penetrations 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, the SparkFun TMP117 breakout (SEN-15805) can be connected via a 100 mm Qwiic cable to the Notecarrier CX. Using the TMP117 requires replacing `#include ` with `#include `, 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 compartment interior temperature. Appropriate only for development and the functional validation described in [Validation and Testing](#9-validation-and-testing).
-
-### Light sensor placement
-
-- For bench evaluation, position the VEML7700 at the exterior of the door opening — near the hinge-side door edge or door frame — with its sensing window facing the interior lamp. At that location the sensor is not inside the cold zone and is not exposed to sustained condensation. Do not permanently mount the bare VEML7700 PCB inside the refrigerated compartment; see [Limitations](#11-limitations-and-next-steps) for production placement options.
-- **Only use the `sensor_disagreement` rule 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 exceeds `door_lux_threshold` while the door switch reads CLOSED — it catches a **stuck-closed or failed-closed reed switch**, not a missing magnet or disconnected switch (those drive D5 HIGH, making `door_open = true`, which does not meet the alert condition). Do not use this rule on always-on-lamp units without raising `door_lux_threshold` to `120000.0` in Notehub; see [Limitations](#11-limitations-and-next-steps).
-
-**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 become visible 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; return the negative rail from Notecarrier CX **GND** back to the supply. Connect Mojo's Qwiic port to the VEML7700 Qwiic OUT connector using the 100 mm bench Qwiic cable listed in [§4 Hardware Requirements](#4-hardware-requirements) for this purpose, daisy-chaining Mojo onto the end of the I²C bus. This arrangement measures the total VBAT rail current (Notecard plus Cygnet when active) and reveals the classic sleep-wake-cellular current profile described in Validation.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/cold_storage_audit_monitor/cold_storage_audit_monitor.ino` line 51 as `PRODUCT_UID`.
-
-2. **Claim the Notecard.** Power the unit; on first cellular session the Notecard associates with your project automatically. Check Notehub **Events** tab to confirm the device has synced.
-
-3. **Create Fleets.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) 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 appropriate temperature thresholds at the fleet level via environment variables so that a single firmware image services all storage classes without recompilation. Use [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) to auto-assign devices based on a device-level environment variable (e.g., `storage_class`).
-
-4. **Set environment variables.** In Notehub, navigate to **Fleet → Environment**. All variables below are optional; firmware defaults are shown. Any variable set in Notehub overrides the compile-time default on the device's next inbound sync — no reflash required.
-
- > **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. A bench unit at ~22 °C will produce zero temperature alerts with these defaults. 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 — no reflash required.
-
- | Variable | Bench default | Production (refrigerated) | Purpose |
- |---|---|---|---|
- | `temp_high_alert_c` | `30.0` | `8.0` | Temperature (°C) above which `temp_excursion_high` fires. Set to `8.0` for USP 659 refrigerated storage (vaccines, biologics). |
- | `temp_low_alert_c` | `15.0` | `2.0` | Temperature (°C) below which `temp_excursion_low` fires. Set to `2.0` for USP 659 refrigerated storage. |
- | `door_open_alert_min` | `10` | `10` | Minutes a door must be continuously open before `door_open_timeout` fires. |
- | `alert_cooldown_min` | `30` | `30` | Minimum minutes between successive alerts of the same type. Prevents alarm fatigue during a slow-developing excursion. |
- | `sample_interval_sec` | `300` | `300` | Seconds between sensor readings (minimum 60 enforced in firmware). Reducing this value increases per-sample Note volume proportionally. At 5 minutes (300 seconds), one bench unit produces ~288 reading Notes per day. |
- | `door_lux_threshold` | `5.0` | `5.0` | Lux value above which the interior is considered lit for the `sensor_disagreement` rule. Set to `120000.0` to disable the rule without a firmware rebuild — required on lamp-free units and on always-on-lamp units where the rule causes persistent false positives. |
-
- **Example:** To set `temp_high_alert_c` to `8.0` in the Fleet Environment, click **+ Add** and enter:
- ```
- Key: temp_high_alert_c
- Type: Number
- Value: 8.0
- ```
-
-5. **Configure routes.** In Notehub, add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `storage_alert.qo` (to an on-call paging system, LIMS, or compliance inbox) and a second for `storage_reading.qo` (to a long-term time-series archive or regulatory data repository). Each `storage_reading.qo` Note represents one 5-minute sample — at the default interval that is 288 Notes per device per day, each individually timestamped. Keeping the two Notefiles separate at the source means each can be delivered to a different destination at a different urgency without any filter logic in the route.
-
- **Example Notehub output** (from the **Events** tab, `storage_reading.qo`):
- ```json
- {
- "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.qo` on excursion):
- ```json
- {
- "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
- },
- "sync": true
- }
- ```
-
-## 7. Firmware Design
-
-The firmware's job is small enough to read end-to-end, but auditors care about every branch — so the sketch is split across three files to keep the sample cycle, the helpers, and the shared state cleanly separated. All three live directly under `firmware/`:
-
-| File | Contents |
-|---|---|
-| [`firmware/cold_storage_audit_monitor/cold_storage_audit_monitor.ino`](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`](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`](firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.cpp) | Sensor reads, env-var parsing, `sendReading`, `sendAlert`, and `goToSleep` implementations |
-
-Dependencies:
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (the `note-arduino` library). Install via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`.
-- [`Adafruit MAX31865`](https://github.com/adafruit/Adafruit_MAX31865) **≥ v1.1.0** (returns `bool` from `begin()`). Install via Library Manager: search "Adafruit MAX31865". Requires Adafruit BusIO.
-- [`Adafruit VEML7700`](https://github.com/adafruit/Adafruit_VEML7700). Install via Library Manager: search "Adafruit VEML7700". Requires Adafruit BusIO.
-
-### Modules
-
-| Responsibility | Function | File |
-|---|---|---|
-| Notecard configuration (`hub.set`, motion-mode quiet) | `notecardConfigure` | `.ino` |
-| Notefile template definition (cold boot only) | `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` |
-| State serialization and host power-down | `goToSleep` | `_helpers.cpp` |
-
-### Sensor reading strategy
-
-- **MAX31865 (PT1000).** The MAX31865 runs in continuous-conversion mode from `begin()` onward — no `dataReady()` poll is needed. Each `readTemperatureC()` call invokes `rtdAmp.temperature(1000, 4300.0)` (PT1000: R₀ = 1000 Ω; Adafruit board Rref = 4300 Ω), then immediately checks `readFault()`. A non-zero fault byte (RTD open, short-to-VCC, short-to-GND, or over/under-voltage) clears the fault register and returns `NAN`; the reading Note's `temp_c` field carries the `−9999` sentinel so downstream analytics can distinguish 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`) configured once in `setup()`, providing 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). Negative returns indicate a communication fault and produce `NAN`; the reading Note's `lux` field carries the sentinel `−1.0` on a faulted sample (lux is always ≥ 0 in normal operation, so −1.0 is unambiguously a fault marker).
-
-- **Reed switch.** A 10 milliseconds software debounce — two reads separated by a short delay — prevents a mechanical contact bounce from registering as a spurious door event.
-
-### Event payload design
-
-One template-backed reading Note (`storage_reading.qo`) enqueued every 5-minute wake; untemplated alert Notes (`storage_alert.qo`) transmitted immediately via `sync:true`. Templates compress each reading to a fixed-length binary record on the wire.
-
-Sample reading Note body (as it appears in Notehub after the cellular session). All eight template fields are always present:
-
-```json
-{
- "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 (preserved through retries so that a retried Note always carries the original sample timestamp in its body, even though the Notecard envelope reflects retry time). `time_valid` is `false` on samples taken before the Notecard RTC has synced with Notehub. `dropped_readings` and `dropped_alerts` are cumulative counters of host-side ring-buffer overflows — they count readings or alerts that could not be enqueued into the Notecard over I²C, not cellular outages (cellular outages are handled transparently by the Notecard's on-device queue). Both counters are reset to 0 after each successful `storage_reading.qo` enqueue; non-zero values in Notehub indicate entries dropped because the host could not reach the Notecard for more than four consecutive wakes.
-
-Sample alert Note body (temperature excursion, immediately synced). All seven body fields are always present:
-
-```json
-{
- "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
- },
- "sync": true
-}
-```
-
-`event_epoch` is the UTC epoch when the alert condition was first detected — preserved across retries so that a retried Note's body always carries the authoritative original trigger time even though the Notecard envelope reflects retry time. `time_valid` is `false` when the Notecard RTC had not yet synced at the moment the alert fired; downstream audit queries should treat those early-boot alerts as commissioning data rather than audit-grade records.
-
-Four alert types are defined: `temp_excursion_high`, `temp_excursion_low`, `door_open_timeout`, and `sensor_disagreement` (interior light ON while door switch reads CLOSED, indicates a possible stuck-closed or failed-closed reed switch; only meaningful on units with a door-actuated interior lamp. See [Limitations](#11-limitations-and-next-steps)). Each is rate-limited by `alert_cooldown_min` to prevent alert fatigue during a sustained excursion.
-
-### Low-power strategy
-
-Sampling every 5 minutes on a line-powered device does not strictly require aggressive power management, but keeping the host asleep the rest of the time has two practical benefits: reduced heat inside the enclosure (a cold-storage monitor that generates meaningful self-heating is a calibration problem), and a firmware pattern that ports cleanly to a battery-backed variant without a rewrite.
-
-After each sample cycle, `goToSleep()` calls `NotePayloadSaveAndSleep`, which serializes the `AppState` struct into the Notecard's flash and issues `card.attn` with `mode:sleep` and the configured sleep duration. The Notecard's ATTN pin then drives the Notecarrier CX enable gate LOW, cutting power to the Cygnet entirely between wakes. 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 — the USB interface and monitoring circuits remain active while VUSB is asserted. The benefit the ATTN-based sleep still delivers on USB-C is **host MCU power-down**: the Cygnet is fully unpowered between wakes, eliminating 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](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/); the quantitative idle table in [§9](#9-validation-and-testing) applies only to the **+VBAT bench configuration with VUSB absent**. The Cygnet-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; measure the actual draw on your bench with a Mojo or current probe before finalising any power budget.
-
-### Retry and error handling
-
-- The initial Notecard configuration on cold boot (`hub.set`) uses a custom retry loop of up to five attempts with a 2-second delay between each, covering the I²C bus-readiness window on cold boot. Both transport failures (NULL response) and Notecard-reported errors are retried, so a transient startup fault cannot permanently skip the configuration step.
-- `fetchEnvOverrides` checks both the response pointer and the `err` field before applying values; a failed env fetch leaves the existing thresholds intact rather than reverting to defaults.
-- Sensor `NAN` returns cause the reading Note to carry per-field sentinels: `temp_c` faults write `−9999`; `lux` faults write `−1.0`. Downstream parsers must check the correct sentinel per field — `−9999` in `temp_c` distinguishes a MAX31865/probe failure or fault from a legitimate near-zero temperature reading, while `−1.0` in `lux` is unambiguously a VEML7700 fault (lux cannot be negative).
-- Alert de-duplication via `alert_cooldown_min` (default 30 minutes per alert type) prevents a sustained excursion from flooding the on-call channel — the first alert fires within one sample cycle of the event; subsequent re-alerts fire only 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 for the Notehub wire and on-device storage, reducing per-Note overhead compared to variable-length JSON. Template field codes: `14.1` = 4-byte IEEE 754 float, `24` = 4-byte unsigned int, `true` = boolean. When the template is active, the Notecard encodes each `storage_reading.qo` Note as fixed-length binary; Notehub decodes it back to JSON for your routes.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", NOTEFILE_READING);
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-// 14.1 = 4-byte IEEE 754 float; 24 = 4-byte unsigned int; true = boolean
-JAddNumberToObject(body, "temp_c", 14.1); // temperature reading in °C
-JAddNumberToObject(body, "lux", 14.1); // ambient illuminance in lux
-JAddNumberToObject(body, "door_open_sec", 24); // seconds door has been continuously open
-JAddBoolToObject(body, "door_open", true); // door state at reading time
-notecard.sendRequest(req);
-```
-
-### Key code snippet 2: immediate-sync alert
-
-`sync:true` bypasses the scheduled outbound window. The Notecard opens a cellular session within seconds of receiving 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 when this is a retried send.
-
-```cpp
-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)temp_c);
-JAddNumberToObject(body, "lux", (double)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);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 3: state persistence across sleep
-
-`NotePayloadSaveAndSleep` serializes the AppState struct into Notecard flash and issues `card.attn` sleep. The next wake calls `NotePayloadRetrieveAfterSleep` in `setup()` to restore door-open timestamps, alert cooldown state, and runtime configuration.
-
-```cpp
-// Saving state and sleeping (end of each sample cycle):
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, state.sample_interval_sec, NULL);
-
-// Restoring state on wake (in setup()):
-NotePayloadDesc payload;
-bool ok = NotePayloadRetrieveAfterSleep(&payload);
-if (ok) {
- ok &= NotePayloadGetSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
- NotePayloadFree(&payload);
-}
-```
-
-### Key code snippet 4: light/door sensor disagreement
-
-The VEML7700 provides an independent second opinion on door state. On a door-actuated-lamp unit, if the interior light is on but the reed switch says the door is closed, this indicates a stuck-closed or failed-closed reed switch — the switch contacts remain closed (D5 LOW) even though the interior lamp is lit. Note that a disconnected switch or displaced magnet would drive D5 HIGH (`door_open = true`) and would NOT trigger this path; those failure modes make the firmware believe the door is open, which is the opposite condition.
-
-```cpp
-bool light_on = (!isnan(lux) && lux > state.lux_threshold);
-if (light_on && !door_open && cooldown_ok) {
- sendAlert("sensor_disagreement", temp_c, lux, false, 0);
-}
-```
-
-## 8. Data Flow
-
-
-
-Every `sample_interval_sec` (default 5 minutes) the Cygnet reads temperature, lux, and door state. Four independent alert rules run against every sample:
-
-- **`temp_excursion_high`** — temperature above `temp_high_alert_c` (bench default 30.0°C; set to 8.0°C in Notehub for USP 659 refrigerated storage). Fired on the first excursion reading; re-arms after `alert_cooldown_min` if the temperature remains out of range.
-- **`temp_excursion_low`** — temperature below `temp_low_alert_c` (bench default 15.0°C; set to 2.0°C in Notehub for USP 659 refrigerated storage). Same cadence.
-- **`door_open_timeout`** — door continuously open for more than `door_open_alert_min` (default 10 minutes). Evaluated by comparing the current epoch against the stored `door_open_since` timestamp.
-- **`sensor_disagreement`** — interior light above `door_lux_threshold` while the reed switch reports closed, indicating a possible stuck-closed or failed-closed switch state. Treated as a maintenance alert rather than a compliance excursion. Only meaningful on units with a door-actuated interior lamp; causes persistent false positives on always-on-lamp units unless `door_lux_threshold` is raised. See [Limitations](#11-limitations-and-next-steps).
-
-**Collected.** On each 5-minute wake: temperature (°C), ambient lux, door-open boolean, current door-open duration (seconds), UTC epoch.
-
-**Transmitted.**
-- `storage_reading.qo` — one templated Note **per wake** (default 288 Notes/day at the 5-minute sample interval). Each Note carries the instantaneous temperature, ambient lux, door state, and the elapsed seconds the door has been continuously open at reading time. Notes accumulate 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 multiple outbound windows, 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 produce gaps in the record (see [Limitations](#11-limitations-and-next-steps)). **This is distinct from the host-side retry ring:** the firmware also maintains a 4-entry ring buffer for the case where the host cannot reach the Notecard over I²C (e.g., a transient bus fault). If the host cannot enqueue into the Notecard for more than four consecutive wakes, the oldest buffered reading is overwritten and counted in `dropped_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 by `alert_cooldown_min`), with `sync:true` to open an immediate cellular session regardless of the scheduled outbound cadence.
-
-**Routed.** Both Notefiles land in Notehub. From there, routes can deliver `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. Because the Notefiles are distinct at the source, no filtering logic is needed in the route configuration — 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 are gated on the Notecard returning a valid UTC epoch from `card.time` (firmware guard: `now > 0`). On first power-on, the Notecard must register on the cellular network and sync time with Notehub before `card.time` returns a non-zero value; this typically takes one to several minutes but can be 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 is not synchronized until the first Notehub session completes, so the `when` field on those early Notes may be inaccurate; treat Notes emitted during this window as commissioning-only data rather than audit-grade records. Door timing specifically requires a valid epoch: if the door is open before `card.time` returns non-zero, `door_open_since` is not set and `door_open_sec` in the reading Note will read zero; duration tracking begins only on the first sample where both the door is seen open and the epoch is valid. Plan for a commissioning warm-up period of at least 5 minutes before relying 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 accumulate in the Notecard queue and are delivered in a batch on each 60-minute cellular outbound session. During commissioning, verify that `temp_c` values in consecutive reading Notes are stable and within the configured range; a `−9999` in `temp_c` indicates a MAX31865/probe fault or SPI failure on that sample, and a `−1.0` in `lux` indicates 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 immediately trip `temp_excursion_high` at room temperature — expected behavior given that the sensor is not inside a refrigerated compartment.
-
-**Threshold smoke test.** Set `temp_high_alert_c` to a value slightly **below** the current actual temperature using the Notehub device environment variable UI. The alert condition is `temp_c > temp_high_alert_c`, so the threshold must be below the current reading for the condition to be met, for example, if the sensor reads 22°C, set `temp_high_alert_c` to `20.0`. The next inbound sync delivers the new threshold; the next sample cycle trips the alert. The `storage_alert.qo` Note should appear in Notehub within one cellular-session window of the alert emission (typically under 60 seconds). Restore the original threshold and confirm that no further 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`. Verify that a `door_open_timeout` alert Note arrives 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 incrementing `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 — do not use those 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 opening the door, so the switch stays CLOSED (D5 LOW) while the interior lamp comes on. Alternatively, force D5 LOW externally (short D5 to GND on the header to simulate a stuck-closed switch) while shining a flashlight at the VEML7700. The firmware should emit a `sensor_disagreement` alert on the next sample cycle.
-
-**Power validation with Mojo.** The [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) 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](#5-wiring-and-assembly) for the complete bench setup). The figures below apply to this **+VBAT bench configuration with VUSB absent only** — they do not represent USB-C deployed operation. Notecard idle and cellular-session figures are drawn from the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) and the Blues [low-power firmware design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/); the Cygnet-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; Cygnet OFF via ATTN) | ~8–18 µA — Notecard in deepest low-power state, Cygnet fully unpowered |
-| Cygnet active (sensor reads, ~2–4 seconds per wake) | ~3–10 mA estimated commissioning target (no factory specification; to be measured on your bench) — Cygnet STM32 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 [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) VMODEM DC characteristics |
-
-On the **+VBAT bench setup (VUSB absent)** a Mojo trace for a healthy 24-hour period shows: a quiet baseline of single-digit µA broken by small milliamp blips every 5 minutes (Cygnet wake), and one larger burst per hour lasting 20–60 seconds (cellular outbound session). If the baseline is persistently above ~1 mA, the Cygnet is not sleeping — investigate the ATTN pin wiring and the `NotePayloadSaveAndSleep` call. If the hourly burst is absent, the Notecard may not have registered on the network; check the antenna connection and `hub.status` via 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 will not measure deployed system current; do not use Mojo on +VBAT when the USB-C supply is present. To measure total system current on a USB-C deployed unit, place an inline USB power meter (or a bench ammeter) in series with the USB-C supply lead. The Notecard idle current on the VUSB rail is higher than the µA bench figures above; consult the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) DC characteristics for the authoritative VUSB-powered idle figure. The quantitative table and the Mojo procedure described above apply strictly to the **+VBAT bench configuration with VUSB absent**.
-
-See the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) and the [Notecard low-power firmware design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for complete, authoritative power figures.
-
-## 10. Troubleshooting
-
-**Device does not appear in Notehub.**
-- Confirm PRODUCT_UID is correctly pasted into `firmware/cold_storage_audit_monitor/cold_storage_audit_monitor.ino` line 51 and that you have flashed the firmware.
-- Check that the SIM card in the Notecard has been activated (all Blues Notecards ship with an active SIM; confirm in the [Blues Shop](https://shop.blues.com/) under your account).
-- Verify 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 appears in the device list but shows no Events, check the cellular signal at that location (weak signal can delay first registration).
-
-**Temperature readings are −9999 or missing.**
-- The MAX31865 amplifier or PT1000 probe has a fault. Verify SPI wiring: CLK, SDI (MISO label on CX), SDO (MOSI label on CX), CS (D10), +3.3V, GND. Note the [Notecarrier CX label swap](#rtd-temperature-amplifier-max31865) — the silkscreen labels MOSI and MISO are reversed; use the pin table in §5, not the labels.
-- If using a bench TMP117 instead, confirm the Qwiic cable is connected and that you have swapped the library from MAX31865 to SparkFun_TMP117 (see [§4](#4-hardware-requirements)).
-- Open the Notehub **Project → Terminal** tab, select your device, and run `card.status` to check if the Notecard is reporting a fault condition.
-
-**Lux readings are −1.0.**
-- The VEML7700 sensor is not responding on the I²C bus. Verify the Qwiic cable is connected from the Notecarrier CX **Qwiic** connector to the VEML7700 **Qwiic IN** connector. Confirm the cable is not kinked or damaged.
-- Check that you are not using a TMP117 in the production I²C chain — the production design has VEML7700 connected 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. Check the Notehub **Events** tab — the first few reading Notes may show `time_valid: false`; during this pre-sync window, no alerts fire. Wait at least 5 minutes after power-on, then test.
-- For temperature alerts: verify that you have overridden `temp_high_alert_c` and `temp_low_alert_c` in the Notehub **Fleet → Environment**. The bench defaults (30°C high, 15°C low) will not fire on a refrigerated unit at 4°C.
-- For door alerts: confirm the reed switch is wired to D5 with a pull-up enabled. Open the door and hold it open for longer than `door_open_alert_min` (default 10 minutes). Check **Notehub → Events** for a `storage_alert.qo` Note with `alert: "door_open_timeout"`.
-
-**The device is consuming too much power.**
-- Verify the Notecard is entering sleep mode. On a +VBAT bench setup (no USB-C), the baseline current should be single-digit µA between samples. If the baseline is persistently > 1 mA, the ATTN pin wiring may be incorrect, or the `NotePayloadSaveAndSleep` call may not be executing. Check the ATTN pin connection from the Notecard to the Notecarrier CX enable gate.
-- On a USB-C powered deployment, the Notecard's idle current is higher than the µA bench figures (see [§9 Power validation](#9-validation-and-testing)). This is expected.
-
-## 11. Limitations and Next Steps
-
-This reference design produces the measurement record and the cellular data path an auditor cares about, but several of the surrounding pieces of a regulated cold-chain program are explicitly out of scope — the calibration certificate that goes with each probe, the SOPs that govern the program, the multi-point mapping that a large freezer needs, and the ultra-cold (−80°C) hardware path. The list below names those boundaries so anyone evaluating this design against a real compliance program can see exactly what they still need to bring.
-
-### Deployment Considerations
-
-The boundaries below are the surrounding pieces of a regulated cold-chain program that this design deliberately leaves to the operator — so anyone evaluating it against a real compliance program can see exactly what they still need to bring.
-
-**Regulatory and compliance scope.** This reference design produces an audit-evidence record of temperature readings, door events, and excursion alerts, and transmits that record to Notehub via 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 required by any specific regulatory body. 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) via the Adafruit MAX31865 PT1000 Amplifier (Product 3648). The PT1000 probe's 316L stainless-steel capsule routes into the refrigerated compartment; 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 receive 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 enter the unit into a recertification schedule matching your calibration management program. The SparkFun TMP117 breakout (SEN-15805) is a bench substitute only. See [§4 Hardware Requirements](#4-hardware-requirements) for the library swap required to use it.
-
-**Single-point temperature measurement.** One PT1000 probe measures a single location in the compartment. USP Chapter 659 and CDC guidelines recommend sensor placement 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 a different SPI chip-select pin (e.g., D9) with a second calibrated PT1000 probe, and a second `temp_c_2` template field, is a straightforward extension.
-
-**No cryptographic Note signing.** The Notecard includes an STSAFE secure element. This design does not implement note-level signing — the `when` timestamps assigned by Notehub are server-side and cannot be altered after delivery, but the Note body itself is not 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 are gated on the Notecard returning a valid UTC epoch (`now > 0`). During initial cellular registration and NTP sync, typically a few minutes on first power-on, potentially longer in marginal signal — the device samples sensors and enqueues reading Notes, but does not fire alerts and does not 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`; `door_open_sec` in those early reading Notes will read 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 will not 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 reduces the blind spot at the cost of more frequent Cygnet 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 is opened and off when it closes. On **lamp-free** units, lux will never exceed 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 (lamp stays lit regardless of door state), `lux > door_lux_threshold` is true whenever the door is closed (the normal closed-door state), so the rule fires persistently every cooldown period — a source of continuous false positives, not 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 deploying this rule.
-
-**VEML7700 placement is bench-only.** The bare Adafruit VEML7700 breakout PCB is not rated for sustained cold or condensing environments. Permanently mounting the unprotected PCB inside a refrigerated cabinet will eventually cause moisture-related corrosion and failure — the same risk that makes the document recommend keeping 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 exterior of the door frame where it detects light spillage when the door is ajar, or use a different secondary door-verification signal (such as a suitably packaged hall-effect sensor). The `sensor_disagreement` feature may be omitted from production builds where a robust cold-tolerant light-sensing solution has not been specified.
-
-**Two independent buffering layers with different depth and failure-mode guarantees** protect the audit record, and it is important to distinguish them clearly for compliance purposes. The first is the **Notecard flash-backed queue**, which handles cellular and WiFi outages transparently: Notes accumulate 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; an extended cellular outage of several consecutive days can exhaust on-device flash storage, producing gaps in the audit-evidence record. Using a `note.template` for `storage_reading.qo` encodes each Note as a compact fixed-length binary record, reducing per-Note storage overhead and improving wire efficiency compared to variable-length JSON; however, on-device queue depth is still finite and bounded by the Notecard's total flash capacity. Configure a shorter `outbound` interval to keep the queue shallow under normal operation. The second is the **host-side I²C retry ring (`PENDING_RING_CAP = 4`)**, which handles only the distinct case where the Cygnet host cannot 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 cannot 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, supplement with a larger host-side spill buffer (FRAM, SD card, or SPI flash) on the Cygnet's SPI bus.
-
-**No power-fail event.** If the facility experiences a power outage and the monitor is not on UPS, both the Notecard and the Cygnet lose power. Any Notes queued but not yet synced remain in the Notecard's flash-backed queue and will be delivered on the next power-on and cellular session, but a gap in the reading series will be visible in Notehub, which is itself a useful signal for the audit-evidence record. Adding a small LiFePO4 backup cell on the +VBAT rail would allow the monitor to survive brief outages and log the power-loss event explicitly.
-
-**Mojo is bench-only.** The firmware does not read the Mojo's LTC2959 coulomb counter via the Qwiic bus. Adding a runtime mAh field to the reading Note is a straightforward extension if fleet-level energy telemetry is valuable.
-
-### Production Next Steps
-
-Once the basic audit monitor is delivering records an auditor trusts, the following extensions are the natural progression — from multi-point coverage to fleet maintenance and the ultra-cold hardware path.
-
-**A second MAX31865 + PT1000 probe** on a different chip-select (e.g., D9) enables multi-point mapping of larger units. The template and per-sample reading logic extend naturally — add a `temp_c_2` field to the template and read both sensors on each wake. Each probe assembly requires its own NIST-traceable calibration certificate.
-
-**[Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** provides over-the-air host firmware updates, essential for maintaining a fleet across regulatory-threshold changes without site visits.
-
-**A `storage_calibration.db` inbound Notefile** delivers per-unit temperature-offset correction from Notehub, eliminating the need to reflash when calibration data changes after annual re-certification.
-
-**Notehub-side webhook routing** integrates the device with 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 covering 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 don't start with a failed sensor — they start with an undocumented excursion that nobody noticed, or a weekend door-open event that no one was around to catch. 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: a NIST-calibrated PT1000 probe inside the compartment, 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 any facility outage and sync the moment cellular returns, so the audit record stays complete even when the WiFi doesn't. The independent cellular path isn't a convenience — in an audit, it's the difference between a complete record and a "we think it was fine" conversation. The same pattern scales from a single pharmacy refrigerator to a fleet across dozens of sites: thresholds per storage class, routes per destination, environment variables per device, 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.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/regulatory-grade-pharmacy-and-lab-cold-storage-audit-monitor/)**
diff --git a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/banner.png b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/banner.png
deleted file mode 100644
index b216e6b1..00000000
Binary files a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/banner.png and /dev/null differ
diff --git a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/01-system-architecture.svg b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index 77691b18..00000000
--- a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/02-wiring-assembly.svg b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index fa065281..00000000
--- a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,91 +0,0 @@
-
diff --git a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/03-data-flow.svg b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index 57901e9d..00000000
--- a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-
diff --git a/62-construction-site-environmental-noise-exposure-monitor/README.md b/62-construction-site-environmental-noise-exposure-monitor/README.md
index 4a36f1d4..31adb972 100644
--- a/62-construction-site-environmental-noise-exposure-monitor/README.md
+++ b/62-construction-site-environmental-noise-exposure-monitor/README.md
@@ -1,424 +1,5 @@
# Construction Site Environmental & Noise Exposure Monitor
-
+A cellular reference design that turns a construction site into a continuously monitored area, tracking PM2.5, PM10, and sound levels in real time without site WiFi or power outlets.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Construction%20Site%20Environmental%20%26%20Noise%20Exposure%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/construction_env_monitor/](firmware/construction_env_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [safety assurance](https://blues.com/safety-assurance/) reference design that turns a construction site into a continuously monitored, GPS-stamped area monitoring point — tracking PM2.5, PM10, and dB(A) sound levels in real time without any site WiFi, power outlets, or IT coordination.
-
-## 1. Project Overview
-
-**The problem.** OSHA regulations governing construction sites require documented monitoring of worker exposure to silica dust, general particulate matter, and noise. OSHA 29 CFR 1926.1153 mandates air monitoring when workers may be exposed to respirable crystalline silica above the action level (25 µg/m³). Similarly, OSHA's noise standard (29 CFR 1926.52) requires monitoring and protection when **TWA** (time-weighted average) noise levels reach 85 dB(A) over an 8-hour shift. Compliant silica exposure documentation requires filter-based personal samplers analyzed by X-ray diffraction (NIOSH Method 7500); compliant noise documentation requires a dosimeter worn by each worker for a full shift. Both paradigms share the same problem: data arrives too late to act on — the shift is over before anyone reviews it, and the opportunity to intervene in real time has passed.
-
-This project is a solar-powered cellular **area monitor** — a fixed, perimeter-mounted enclosure that tracks PM2.5, PM10, and dB(A) levels continuously and transmits geo-stamped summaries over cellular. It **complements, rather than replaces, personal silica sampling and noise dosimetry**: when the area monitor flags an elevated-dust or high-noise condition, a safety officer can immediately redirect workers, deploy respirators or hearing protection, and dispatch personal monitoring for the affected crew. A **PM2.5/PM10** optical particle sensor and an analog sound level meter sample every 5 minutes near the active exposure zone. Readings accumulate in the device and transmit as a geo-stamped summary every 30 minutes. If a threshold is breached — say, a concrete saw fires up near the sensor — an alert Note is transmitted immediately after the sampled breach is detected. Worst-case detection latency is one full sample interval (5 minutes) plus measurement time (~55 seconds), after which the alert leaves the device over cellular rather than waiting hours for an end-of-shift download.
-
-**Why [Notecard](https://shop.blues.com/products/notecard-cellular?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link).** Construction sites are, by definition, WiFi-free environments. There's no permanent network infrastructure to connect to, no IT department to issue credentials, and the general contractor (GC) is responsible for compliance across a dozen subcontractors who bring their own tools and their own hazards to the same site. The GC can't rely on a sub's WiFi hotspot, a temporary access point that may be moved, or a site trailer that's offline half the time. Cellular is the only connectivity a GC can deploy and own with their own equipment. The Notecard Cell+WiFi's prepaid global cellular removes the per-site SIM procurement headache, and the same hardware — same firmware, same enclosure — deploys identically across every site in a GC's portfolio without a single network-form to fill out. When the site moves in three months, the enclosure moves with the crew.
-
-
-
-**Deployment scenario.** A weatherproof NEMA 4X enclosure mounted on a temporary fence post, T-bar stake, or equipment cage near the active work area. For dust monitoring, position the sensor **downwind** of the dust-generating task so the plume passes through the sensor rather than away from it — upwind placement will miss the plume entirely. For noise monitoring, place the enclosure within 5–10 meters of the primary noise source. For **particulate** background subtraction, deploy a second unit upwind of the site as a baseline; the arithmetic difference between the two PM concentration readings (µg/m³) approximates the task-generated particulate contribution. A second sound monitor deployed upwind or in a quieter reference area is useful contextual information, but dB(A) values cannot be meaningfully background-subtracted by simple arithmetic. Decibel levels are logarithmic representations of acoustic energy, so isolating the task-generated contribution means working in the energy domain: convert to intensity in W/m², subtract, then convert back. Direct subtraction of dB numbers is acoustically incorrect. A 6V solar panel is surface-mounted on the enclosure lid. The LiPo battery inside provides overnight autonomy between solar charging sessions. No shore power, no network drops, and no daily downloads.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The order of operations on the Cygnet STM32 host matters because the PM sensor's fan would contaminate the sound measurement if it ran during the dB(A) window. Every 5 minutes the host wakes, samples sound for 15 seconds first — before any I²C traffic starts the PMSA003I — then initializes the PM sensor, waits 30 seconds for laser and fan to stabilize, and averages 10 readings over the next 10 seconds. Every sample folds into a rolling window kept in Notecard flash so it survives the sleep, and between cycles [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) cuts Cygnet power entirely, dropping host current to essentially zero. Queued Notes ride from the Cygnet to the Notecard over I²C using the `note-arduino` library's `JAdd*` helpers — no JSON hand-marshaling, no modem AT commands.
-
-**Notecard responsibilities.** The Notecard buffers each [Note](https://dev.blues.io/api-reference/glossary/#note) in its on-device queue, opens a cellular session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 30 minutes), and treats any `sync:true` alert Note as an immediate interrupt — a saw fires up near the sensor, and the alert leaves the device on a session opened in the next few seconds. The Notecard also owns GNSS: its built-in receiver acquires a site fix on first boot, re-acquires every 4 hours by default, and embeds the last-known fix in every outbound Note. Because the receiver retains a cached fix across power cycles, the first `card.location` response after the enclosure is moved to a new site can still return the previous site's coordinates — the firmware guards against that by treating the first non-zero response after boot as a baseline and flipping `location_valid` to `true` only when a subsequent response carries a newer `time` timestamp. See [Limitations](#10-limitations-and-next-steps). [Environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) flow back from the [Blues Notehub](https://blues.com/notehub/) cloud service on each inbound sync, so a GC can retune the dB(A) or PM thresholds on the fly without rolling a truck to the site.
-
-**Notehub responsibilities.** Each enclosure's embedded global SIM gets it onto carrier cellular worldwide and delivers data to [Notehub](https://notehub.io) over the Internet. Notehub ingests every event, stores it, and applies project-level routes. Exposure summaries (`env_summary.qo`) and threshold alerts (`env_alert.qo`) land in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) so the safety officer's inbox and the long-term compliance archive can be served from the same device without any per-Note filter logic in between. Notehub also appends `where_lat`, `where_lon`, and related location metadata to every event from the Notecard's last-known GNSS fix.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP Pub/Sub, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing documentation](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for details — this project does not ship any specific downstream endpoint.
-
-## 3. Technical Summary
-
-1. **Clone and configure**: Download this repo and open `firmware/construction_env_monitor/construction_env_monitor.ino`. Replace `PRODUCT_UID` constant with your Notehub project UID (see §6 step 1).
-2. **Build and flash**: Use Arduino IDE or `arduino-cli`. The FQBN below matches `firmware/construction_env_monitor/sketch.yaml`, so omitting `--fqbn` also works when invoked from the sketch directory:
- ```bash
- arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/
- arduino-cli upload -p /dev/ttyUSB0 --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/
- ```
-3. **Power and observe**: Connect LiPo (or USB for bench testing). The device samples every 5 minutes, queues summaries, and transmits to Notehub every 30 minutes. Watch `env_summary.qo` Notes arrive in Notehub within 30 minutes of first power-on.
-4. **Set thresholds**: In Notehub, navigate to your Fleet → Environment Variables and set `pm25_alert_ug_m3`, `pm10_alert_ug_m3`, and `db_a_alert` (see §5). Alerts fire immediately when thresholds are breached.
-
-**What you'll have:** A solar-powered cellular area monitor that streams PM2.5, PM10, and sound levels as templated Notes to Notehub, with real-time alerts on threshold breach.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "env_summary.qo",
- "body": {
- "pm25_avg": 18.3,
- "pm25_peak": 42.7,
- "pm10_avg": 31.1,
- "pm10_peak": 89.4,
- "pm_samples": 6,
- "db_a_avg": 71.2,
- "db_a_peak": 83.6,
- "samples": 6,
- "voltage": 3.91,
- "lat": 37.774,
- "lon": -122.419,
- "location_valid": true
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Type | Rationale |
-|------|-----|------|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Required | Integrated carrier with an embedded Cygnet STM32 host, LiPo JST connector, Qwiic I²C port, and 6-channel ADC header — no separate MCU needed. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) / [datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) | 1 | Required | Cellular removes per-site WiFi dependency; the MBGLW includes built-in GNSS for geo-stamping every reading without a separate GPS module. |
-| [Blues Flexible LTE, Wi-Fi, or GPS/GNSS Antenna](https://shop.blues.com/products/flexible-cellular-or-wi-fi-antenna?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 2 | Required | One for the MBGLW **MAIN** (cellular) u.FL port; one for the **GPS** u.FL port. The same Molex-manufactured Blues flexible antenna covers both: its 698 MHz–4.0 GHz broadband range includes GPS L1 at 1575 MHz. The MBGLW also has a **WIFI** u.FL port; that port is left unconnected in this design — cellular is the sole wireless path and the WiFi radio is not used. Because the Hammond 1554CGY enclosure is ABS plastic (RF-transparent), both flex antennas mount **inside** the enclosure on interior surfaces; no cable glands or external mounting are required for the antennas. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Optional (bench only) | Coulomb counter for bench-side energy validation; confirms that sleep current, active sampling current, and per-session transmit energy match expected figures. Not required for field deployment. |
-| [Adafruit PMSA003I Air Quality Breakout (#4632)](https://www.adafruit.com/product/4632) | 1 | Required | I²C optical particle counter reporting PM2.5 and PM10 in µg/m³. The Adafruit breakout includes an onboard 5V boost circuit so it runs correctly from the Notecarrier CX's 3.3V Qwiic rail. |
-| [DFRobot Gravity Analog Sound Level Meter (SEN0232)](https://www.dfrobot.com/product-1663.html) | 1 | Required | A-weighted analog sound meter, 30–130 dB(A), 3.3–5 V supply, analog voltage output. Op-amp output connects directly to the Cygnet's 12-bit ADC with no additional circuitry required. |
-| [SparkFun Sunny Buddy MPPT Solar Charger (PRT-12885)](https://www.sparkfun.com/products/12885) | 1 | Required | Maximum power point tracking solar charger for a single-cell LiPo. Extracts maximum current from the panel across varying light conditions — critical for a north-facing or partially shaded site fence mount. |
-| [Adafruit 6V 6W Solar Panel (#1525)](https://www.adafruit.com/product/1525) | 1 | Required | Weatherproof monocrystalline panel, 930 mA peak. At 5 peak-sun hours per day, generates well over the daily energy budget for this duty-cycled system. Mounts flat on the NEMA enclosure lid with included screws. |
-| [Adafruit Lithium Ion Polymer Battery 3.7V 1200 mAh (#258)](https://www.adafruit.com/product/258) | 1 | Required | Single-cell LiPo with 2-pin JST-PH connector. Validate actual system draw with the Mojo bench exercise in §8 before sizing for deployment. |
-| JST-PH 2-pin pigtail cable, female connector to bare-wire leads, ~100–150 mm (e.g., Adafruit #3814 or equivalent) | 1 | Required | Connects the Sunny Buddy `LOAD+`/`LOAD–` through-hole pads to the Notecarrier CX LiPo JST receptacle. Solder the bare-wire end to the LOAD pads; plug the JST-PH female connector into the Notecarrier's battery input. |
-| Qwiic / STEMMA QT cable, 100 mm | 2 | Required | One connects the Notecarrier CX Qwiic port to the PMSA003I STEMMA QT connector for permanent field use. A second cable (optional, bench only) continues the daisy-chain from the PMSA003I's second STEMMA QT port to the Mojo's Qwiic port, enabling I²C readback of the Mojo's LTC2959 coulomb counter during validation. See §4 bench wiring and §8. |
-| NEMA 4X weatherproof enclosure, ~8×6×3″ (e.g., Hammond Manufacturing 1554CGY, ~9.4×6.0×3.1″, NEMA 4X ABS) | 1 | Required | IP66-rated housing for outdoor mounting. Requires sensor-port hardware listed below to maintain weather resistance once openings are cut. |
-| IP-rated sintered-PE vent, M20 or M16 (e.g., Würth Elektronik 3800301 or equivalent) | 2 | Required | Provides PM2.5/PM10 sensor airflow (inlet + exhaust) while preserving the enclosure's weather rating. Sintered-PE construction passes particulate-laden air for measurement without allowing bulk water ingress. |
-| Weatherproof acoustic vent plug, 20–25 mm PTFE-membrane type (e.g., Würth Elektronik WE-AVE 3820101 or equivalent) | 1 | Required | Allows sound pressure waves to reach the SEN0232 microphone capsule while sealing against rain and splash. Mount with the board's microphone aperture aligned directly behind the vent opening. A bare hole — even with foam packing — does not maintain IP/NEMA weather resistance. |
-
-All Blues parts ship with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-The whole enclosure has to survive being bolted to a fence post in the rain and operate on whatever sun the site gives it, so the build is small: a Notecarrier CX with the Notecard Cell+WiFi (MBGLW) in its M.2 slot, two sensors on opposite ends of the I²C and analog buses, and a solar power chain on the back. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin headers. The MBGLW exposes three u.FL connectors — **MAIN** (cellular), **GPS** (GNSS), and **WIFI**; MAIN and GPS each take an antenna in this design, and WIFI stays unconnected because cellular is the sole wireless path. See antenna routing below.
-
-**Power chain (solar → LiPo → Notecarrier):**
-
-The Sunny Buddy is the sole battery charger in this design. Its `BAT` JST port connects to the LiPo for charging; its `LOAD` through-hole pads on the right edge supply the load (the Notecarrier CX system rail) from the same battery bus. No Y-splitter is needed.
-
-1. Connect the solar panel's positive lead to the Sunny Buddy `VIN+` screw terminal; negative to `VIN–`.
-2. Plug the LiPo battery's JST-PH connector into the Sunny Buddy `BAT` port. The Sunny Buddy's MPPT circuit charges the battery through this connection and is the only charger in the loop.
-3. Wire the Sunny Buddy `LOAD+` and `LOAD–` through-hole pads to the Notecarrier CX LiPo JST connector using a short JST-PH 2-pin pigtail cable (solder the pigtail's bare leads to the LOAD pads):
- - **Bench with Mojo (inline power + Qwiic readback):** Sunny Buddy `LOAD` → Mojo `BAT` input → Mojo `LOAD` output → Notecarrier CX LiPo JST connector. The Mojo sits inline on the power rail and its LTC2959 coulomb counter accumulates the charge drawn from the LiPo. To read the accumulated mAh, add a second 100 mm Qwiic cable from the PMSA003I's second STEMMA QT port to the Mojo's Qwiic port, completing the daisy-chain: Notecarrier CX Qwiic → PMSA003I → Mojo. With Notecard firmware v8.1.3 or later, the Notecard auto-detects the Mojo on the shared I²C bus and periodically logs energy consumption data (including `milliamp_hours`) to `_log.qo` Notes. See the [Mojo documentation](https://dev.blues.io/datasheets/mojo-datasheet/) for details. For a one-shot bench reading, issue `{"req":"card.power"}` from the Notecard's in-browser serial terminal; the firmware in this project does not call `card.power` (see §9 for the extension path).
- - **Deployed (without Mojo):** Sunny Buddy `LOAD` → Notecarrier CX LiPo JST connector directly.
-
-
-
-**USB-C conflict warning.** When the Sunny Buddy LOAD is wired to the Notecarrier CX LiPo JST, **do not simultaneously connect USB-C** to the Notecarrier. USB-C activates the Notecarrier's onboard LiPo charger, which would fight the Sunny Buddy for control of the battery. For USB-only bench sessions (programming, Notecard debug serial): disconnect the Sunny Buddy LOAD pigtail from the Notecarrier LiPo JST first, then connect USB-C alone.
-
-
-
-**PM sensor (Adafruit PMSA003I, I²C):**
-
-- Connect a 100 mm Qwiic cable from the Notecarrier CX's Qwiic port to the PMSA003I's STEMMA QT connector. The Qwiic bus carries 3.3V power, GND, SDA, and SCL in a single connector. The PMSA003I's onboard boost circuit handles the internal 5V requirement.
-- The PMSA003I must have a clear airflow path to sample ambient air. Mount it near the **inlet vent** on the enclosure wall with its sensing aperture facing the vent opening. Cut a second opening on the opposite or adjacent wall for exhaust — the sensor's internal fan creates a pressure differential that requires both an inlet and an outlet. Fit both openings with IP-rated sintered-PE vent plugs (see BOM) to preserve the enclosure's weather rating. Position the inlet on a sheltered face (bottom or side, not the lid) to minimize direct rain exposure.
-
-**Sound level sensor (DFRobot SEN0232, analog):**
-
-- **VCC** → Notecarrier CX `V+` header pin (raw LiPo voltage, ~3.7–4.2 V). The SEN0232 is rated for 3.3–5V operation; using `V+` rather than 3.3V gives the op-amp more output headroom, particularly for high-SPL readings near the 130 dB ceiling.
-- **GND** → Notecarrier CX `GND` header pin.
-- **OUT** → Notecarrier CX `A0` header pin (ADC input).
-- The SEN0232 is a PCB module with an **onboard microphone capsule** — there is no separate remote microphone cable. Mount the entire SEN0232 board inside the enclosure with its microphone aperture aligned directly behind the acoustic vent opening. Cut a 20–25 mm diameter hole on the most sheltered enclosure face and fit it with a **weatherproof acoustic vent plug** (PTFE-membrane type; see BOM). Position the SEN0232 so its microphone port faces the vent membrane with a gap of 2–5 mm. The PTFE membrane passes sound pressure waves while sealing against moisture; a bare port or foam packing will compromise the NEMA 4X weather rating.
-
-
-
-**Enclosure weather rating Note.** Any opening cut for sensor airflow or sound transmission must be fitted with the IP-rated vent components listed in the BOM. Without those components, the enclosure is de-rated to at most IP54 at the sensor ports. The sintered-PE inlet/exhaust vents and the PTFE acoustic membrane vent maintain the enclosure's original weather rating at each opening.
-
-
-
-**Antenna routing:**
-
-The Hammond 1554CGY enclosure is ABS plastic, which is RF-transparent — cellular and GPS signals pass through it without meaningful attenuation. Mount both flexible antennas **inside** the enclosure using their adhesive backing:
-
-- Peel the adhesive backing and press the **cellular** antenna flat against any interior wall surface, with the u.FL pigtail routed neatly back to the MBGLW's **MAIN** connector. Maintain at least 11 mm clearance between the antenna trace and any metal hardware or PCB ground planes.
-- Press the **GNSS** antenna flat against the interior of the **top face** of the enclosure (the upward-facing surface when the unit is deployed upright on a fence post or T-bar stake). The lid carries the solar panel on its exterior — the panel's glass, backing, and frame are not RF-transparent and will attenuate or detune the GNSS antenna if it is placed directly behind the panel. The top face is unobstructed and provides a clear RF path to the sky. Plug the pigtail into the MBGLW's **GPS** connector.
-- Leave the MBGLW's **WIFI** u.FL port unconnected; the WiFi radio is not used in this design.
-
-
-
-**Metal enclosure substitution.** If you replace the ABS enclosure with a steel or aluminum alternative, the antennas must exit the box — route u.FL pigtails through IP-rated cable glands to externally mounted antennas, as metal fully blocks both cellular and GPS signals inside.
-
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into the `PRODUCT_UID` constant in the firmware (line ~15 of `construction_env_monitor.ino`).
-2. **Claim the Notecard.** Power the enclosure. On first cellular session the Notecard automatically associates with your project using the ProductUID. You will see the device appear on the Notehub dashboard within 1–2 minutes.
-3. **Create a Fleet per site.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) (and [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules)) group devices for shared configuration and routing. A natural unit is one fleet per active construction site — all sensors on that site share the same threshold environment variables, which can be tuned to the specific trade activities happening there this week. When a site closes, archive the fleet; when a new site opens, create a fresh one. To add the device to a fleet: Notehub dashboard → Devices → select your device → assign to Fleet.
-4. **Set environment variables.** All variables below are optional; firmware defaults are shown. To set them: Notehub dashboard → Fleets → select your fleet → Settings → Environment Variables → Add. Any value pushed through Notehub overrides the firmware default on the device's next inbound sync — thresholds can be retightened or relaxed without a firmware update or a truck roll to the site. The device syncs environment changes within 30 minutes; to force an immediate sync, issue `{"req":"hub.sync"}` from the Notecard's in-browser serial terminal (accessible via Notehub → Devices → select device → In-Browser Serial).
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_sec` | `300` | Seconds between PM + sound samples. |
- | `report_interval_min` | `30` | Minutes between `env_summary.qo` Notes. When changed, the firmware re-issues `hub.set` on the next wake to update the Notecard's outbound sync cadence. |
- | `pm25_alert_ug_m3` | `35.0` | PM2.5 threshold (µg/m³) above which `pm25_high` fires. |
- | `pm10_alert_ug_m3` | `150.0` | PM10 threshold (µg/m³) above which `pm10_high` fires. |
- | `db_a_alert` | `85.0` | dB(A) threshold above which `db_a_high` fires. |
- | `gps_interval_sec` | `14400` | Seconds between GPS re-acquisition attempts (default 4 h). When changed, the firmware re-issues `card.location.mode` on the next wake. |
- | `db_cal_offset` | `0.0` | Signed dB bias added to all sound readings for field calibration against a reference meter. |
-
-5. **Configure routes.** Add a [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `env_alert.qo` to a real-time safety notification channel — email, Slack, SMS gateway, or a CMMS ticketing endpoint. Add a second route for `env_summary.qo` to a long-term analytics store or compliance archive. Keeping the two Notefiles separate at the source means you never need filter logic in the route; just point each Notefile at its appropriate destination.
-
-## 7. Firmware Design
-
-The firmware spends most of its time asleep, so the interesting code is the wake cycle: sample sound first, sample PM second, fold both into a rolling window, check the three alert thresholds, and decide whether to fire an immediate alert or wait for the next 30-minute summary. Everything else — Notecard config, environment-variable refresh, GNSS handling — runs around that core loop.
-
-### Build and Flash
-
-**Prerequisites:**
-- Arduino IDE 2.0+ with [Arduino core for STM32](https://github.com/stm32duino/Arduino_Core_STM32) installed, or `arduino-cli` v0.21+.
-- Required libraries:
- - `Blues Wireless Notecard`: `arduino-cli lib install "Blues Wireless Notecard"`
- - `Adafruit PM25 AQI Sensor`: `arduino-cli lib install "Adafruit PM25 AQI Sensor"`
-
-**Using Arduino IDE:** Open `firmware/construction_env_monitor/construction_env_monitor.ino`, select board **STMicroelectronics STM32 → Blues → Cygnet (Notecarrier CX)** under **Tools → Board**, update `PRODUCT_UID`, and click Upload. The IDE picks up `sketch.yaml` automatically when the sketch directory is opened, so the board selection is pre-pinned to the correct variant.
-
-**Using `arduino-cli`:**
-```bash
-arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/
-arduino-cli upload -p /dev/ttyUSB0 --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/
-```
-(Adjust `-p` to your serial port; on Windows use `COM3`, on macOS use `/dev/tty.usbserial-*`. The FQBN above is the canonical match for the Cygnet host on the Notecarrier CX, older `Nucleo_L433RC_P` or generic `GenL0` board variants do not match the Cygnet's chip family or peripheral mapping and should not be substituted.)
-
-### Source Files
-
-- [`construction_env_monitor.ino`](firmware/construction_env_monitor/construction_env_monitor.ino) — global state definitions, `setup()`, `loop()`
-- [`construction_env_monitor_helpers.h`](firmware/construction_env_monitor/construction_env_monitor_helpers.h) — shared constants, `AppState` struct, extern declarations, function prototypes
-- [`construction_env_monitor_helpers.cpp`](firmware/construction_env_monitor/construction_env_monitor_helpers.cpp) — sensor helpers, Notecard config helpers, note-send helpers
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| First-boot Notecard configuration (initial `hub.set`) | `notecardConfigure` |
-| Note template registration (idempotent, every wake) | `defineTemplates` |
-| Environment-variable fetch + range clamp per wake | `fetchEnvOverrides` |
-| Outbound-cadence / GPS-cadence re-apply when env vars change | `applyCardConfig` (called from `setup()` and `loop()`) |
-| GPS position refresh from `card.location` | `updateGPS` |
-| PM2.5/PM10 averaging from PMSA003I | `readPmSensor` |
-| Sound level averaging from SEN0232 ADC | `readSoundLevelDb` |
-| Rolling window accumulation, threshold checks, alert emission | `runOneSampleCycle`, `sendAlert` |
-| Periodic exposure summary | `sendSummary` |
-| Persistent state across sleep cycles | `AppState` + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### Sensor reading strategy
-
-**Sound level — sampled first.** `analogReadResolution(12)` is called once in `setup()` to enable the Cygnet's 12-bit ADC. The SEN0232 output voltage maps linearly to dB(A). The firmware samples at 4 Hz for 15 seconds (60 samples) at the very start of the measurement phase — before `begin_I2C()` is called on the PMSA003I. By taking the dB(A) window before the PM sensor's 30-second warm-up phase begins, the fan's acoustic contribution is bounded to at most the host boot + `setup()` time (~5–15 seconds) in the case where the Qwiic rail is gated during ATTN sleep. Whether that rail is actually gated is carrier-implementation-specific — if it is not, the fan runs continuously and contaminates the measurement regardless of sampling order. For complete, guaranteed acoustic isolation, add a GPIO-controlled load switch to the PMSA003I power line so the fan is definitively off during the sound window. See [Limitations](#10-limitations-and-next-steps). For each of the 60 ADC samples, the firmware maps the output voltage to dB(A) via the SEN0232's linear Vout-to-dB transfer function, applies the `db_cal_offset` calibration bias, then converts to acoustic energy (`10^(dB/10)`) and accumulates the result in a running sum. At the end of the 15-second window the mean energy is converted back to dB(A) via `10·log10(meanEnergy)`, yielding the acoustically correct mean sound pressure level. Averaging in the energy domain matters: a brief 100 dB event carries 100× more intensity than 80 dB background; a simple arithmetic mean of the two dB values gives 90 dB, while the energy-domain mean correctly gives ~97 dB. The 15-second window also acts as a natural noise floor smoother, attenuating single-spike artefacts from vehicles or tools passing the sensor.
-
-**PM sensor — sampled after the sound window.** The PMSA003I requires a 30-second warm-up delay (`PM_WARMUP_MS`) for its fan and laser to stabilize before readings are valid. Whether the Notecarrier CX ATTN sleep path cuts the Qwiic/3.3V rail between wakes is carrier-implementation-specific and not guaranteed — do not rely on implicit rail-gating to power-cycle the PMSA003I between samples. The firmware always applies the full `PM_WARMUP_MS` delay after every successful `begin_I2C()` call, ensuring valid readings regardless of whether the sensor cold-started or was already running. After warm-up, 10 consecutive readings are taken at 1 Hz and averaged. `data.pm25_standard` and `data.pm10_standard` (standard-atmosphere-corrected values) provide a stable, comparable PM metric for trend monitoring — they are more consistent than the `_env` variants, which apply an additional humidity-based environmental correction. Neither output is suited for silica-compliance reporting; see [Limitations](#10-limitations-and-next-steps). Use the Mojo to measure the actual idle current floor during ATTN sleep (§8): if the floor is elevated above the always-on SEN0232 quiescent draw, the PMSA003I is being supplied during sleep and drawing continuous fan current.
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates store records as fixed-length binary on the Notecard rather than free-form JSON, reducing on-wire payload by roughly 3–5×.
-
-`pm25_avg` and `pm10_avg` are computed over the subset of wake cycles in which the PM sensor returned a valid read (tracked separately in `AppState.pmSampleCount`), so a failed sensor read on one cycle does not dilute the window mean. `pm_samples` carries that valid-read count as an explicit validity field. When `pm_samples` is `0`, every read in the window failed: `pm25_avg` and `pm10_avg` are emitted as `–9999.0` — an unambiguous invalid-data sentinel that no downstream consumer can mistake for genuinely particulate-free air. `db_a_avg` is computed over all wake cycles (`sampleCount`), since the ADC is always sampled. Example summary body:
-
-```json
-{
- "file": "env_summary.qo",
- "body": {
- "pm25_avg": 18.3,
- "pm25_peak": 42.7,
- "pm10_avg": 31.1,
- "pm10_peak": 89.4,
- "pm_samples": 6,
- "db_a_avg": 71.2,
- "db_a_peak": 83.6,
- "samples": 6,
- "voltage": 3.91,
- "lat": 37.774,
- "lon": -122.419,
- "location_valid": true
- }
-}
-```
-
-Example alert body:
-
-```json
-{
- "file": "env_alert.qo",
- "body": {
- "alert": "pm25_high",
- "value": 48.2,
- "threshold": 35.0,
- "lat": 37.774,
- "lon": -122.419,
- "location_valid": true
- },
- "sync": true
-}
-```
-
-Notehub additionally appends `where_lat`, `where_lon`, `where_location`, and `where_timezone` to every event from the Notecard's last-known GNSS fix. The explicit `lat`/`lon` fields in the payload body are belt-and-suspenders: they survive even if Notehub metadata is stripped during a downstream route transformation. `location_valid` is `false` until the GNSS confirms a new fix at the current site (the GPS `time` timestamp advances since boot); downstream consumers should hold off geo-stamping records or triggering geo-fenced alerts until this field is `true`. In normal outdoor deployment, `location_valid` becomes `true` within one or two 5-minute sample cycles once the GNSS acquires a fresh position.
-
-### Low-power strategy
-
-Even on a solar-powered site, keeping the host asleep between samples reduces average current draw significantly and keeps the enclosure cooler in direct sun. After completing a sample cycle, the host calls `NotePayloadSaveAndSleep`, which serializes the runtime state into Notecard flash (preserving rolling window accumulators, countdown timers, and the last GPS fix) and then triggers `card.attn` to cut host power entirely for `sample_interval_sec` seconds. Between cellular sessions, the Notecard itself idles at ~8–18 µA @ 5V — effectively negligible. The host is awake for approximately 55 seconds per 5-minute cycle (~18% duty cycle), and the cellular session fires once per 30-minute report window.
-
-Sampling cadence and transmit cadence are deliberately decoupled: the firmware samples every 5 minutes but only transmits once per 30 minutes. Actual data consumption depends on sync cadence, signal conditions, Notehub routing behavior, and alert frequency; validate against Notehub's usage dashboard once the device is deployed in its target environment.
-
-### Retry and error handling
-
-- The first Notecard transaction in `notecardConfigure()` uses [`sendRequestWithRetry(req, 5)`](https://dev.blues.io/tools-and-sdks/firmware-libraries/arduino-library/) with a 5-second retry window to paper over the cold-boot I²C race between the Cygnet and the Notecard. Immediately after, `applyCardConfig()` re-issues `hub.set` with `requestAndResponse` because `state.lastReportMin` is initialized to 0 on first boot — this guarantees the outbound cadence is confirmed with a response-checked request even if `notecardConfigure()` suffered a transient failure. `product` is included in every `hub.set` from `applyCardConfig()` so the device can recover from a failed first-boot provisioning on any subsequent wake without a hard reset.
-- `readPmSensor()` counts valid reads independently and returns `false` if zero valid readings are obtained (e.g., sensor not detected on I²C). The calling code in `setup()` guards against adding a -1.0 reading to the accumulators.
-- `env.get` and `card.location` responses are NULL-checked before use; a failed response is silently skipped so a transient I²C error on one wake doesn't corrupt the persistent state.
-- Alert de-duplication via a 30-minute per-type cooldown (`ALERT_COOLDOWN_SEC`) prevents a sustained high-dust or high-noise condition from generating continuous alerts. One alert per type per 30 minutes is enough to notify the safety officer; it's not enough to flood their inbox.
-- If the Notecard is not wired for ATTN-controlled sleep (e.g., bench testing over USB-C only), `NotePayloadSaveAndSleep` returns without cutting power. The `saveStateAndSleep()` call at the end of `setup()` then falls through, and `loop()` takes over: it delays for the remaining trimmed interval, re-applies any changed config, runs another full sample cycle, and sleeps again, so bench mode still produces repeated periodic readings without re-flashing or wiring changes.
-
-### Key code snippet 1 — template definition
-
-Templates must be defined once at first boot. `14.1` is the note-c `TFLOAT32` hint (4-byte IEEE 754 float); `22` is `TUINT16` (2-byte unsigned int). Boolean fields are declared by passing the literal `true` (there is no numeric type-hint for `TBOOL`). See the [note-c template data-type table](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#understanding-template-data-types).
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "env_summary.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "pm25_avg", 14.1);
-JAddNumberToObject(body, "pm25_peak", 14.1);
-JAddNumberToObject(body, "pm10_avg", 14.1);
-JAddNumberToObject(body, "pm10_peak", 14.1);
-JAddNumberToObject(body, "db_a_avg", 14.1);
-JAddNumberToObject(body, "db_a_peak", 14.1);
-JAddNumberToObject(body, "samples", 22);
-JAddNumberToObject(body, "pm_samples", 22); // 0 = all PM reads failed; pm25_avg/pm10_avg = -9999.0
-JAddNumberToObject(body, "voltage", 14.1);
-JAddNumberToObject(body, "lat", 14.1);
-JAddNumberToObject(body, "lon", 14.1);
-JAddBoolToObject(body, "location_valid", true); // TBOOL — declared by passing literal 'true'
-notecard.sendRequest(req);
-```
-
-### Key code snippet 2 — immediate-sync alert
-
-`sync:true` tells the Notecard to bypass the outbound window and open a cellular session right now. This is the mechanism that gets a dust spike off the device within seconds of the threshold crossing. `full:true` opts the Note out of the [omitempty rule](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#use-of-in-templates) that templated Notefiles otherwise apply on Notehub serialization — without it, `location_valid: false` (the explicit "GPS not yet confirmed at this site" signal) and any zero-valued `lat`/`lon` would be silently stripped from the body that downstream consumers receive. The same `full:true` flag is set on the `env_summary.qo` `note.add` so that `pm_samples: 0` (the companion sentinel to `pm25_avg/pm10_avg = -9999.0` when every PM read in the window failed) survives serialization.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "env_alert.qo");
-JAddBoolToObject(req, "sync", true);
-JAddBoolToObject(req, "full", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "pm25_high");
-JAddNumberToObject(body, "value", pm25);
-JAddNumberToObject(body, "threshold", cfgPm25Alert);
-JAddNumberToObject(body, "lat", state.siteLat);
-JAddNumberToObject(body, "lon", state.siteLon);
-JAddBoolToObject(body, "location_valid", state.gpsBootConfirmed);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 3 — sleep between samples
-
-`NotePayloadSaveAndSleep` writes state to Notecard flash and then triggers `card.attn` to cut the Cygnet's power rail. The next `setup()` call rehydrates state with `NotePayloadRetrieveAfterSleep`. The sleep duration is the configured sample interval minus the seconds the host was already awake this cycle, keeping sample starts on the configured cadence.
-
-```cpp
-// In saveStateAndSleep() — construction_env_monitor_helpers.cpp
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, sleepSec, NULL);
-// Returns here only if card.attn is absent (bench mode).
-```
-
-## 8. Data Flow
-
-
-
-Every 5 minutes (default), the Cygnet wakes, reads both sensors (sound first, then PM), accumulates sample statistics, checks three alert thresholds, optionally sends an immediate alert, checks the report timer, optionally sends a summary, and goes back to sleep.
-
-**Collected.** PM2.5 and PM10 in µg/m³ (standard atmospheric); mean dB(A) over a 15-second audio window; peak PM2.5, PM10, and dB(A) values observed in the current reporting window; battery voltage; GPS lat/lon.
-
-**Transmitted.**
-- `env_summary.qo` — once per `report_interval_min` (default 2 Notes per hour, one every 30 minutes); template-encoded, queued in Notecard and flushed by the periodic `outbound` session. PM averages (`pm25_avg`, `pm10_avg`) are computed over valid PM-sensor reads only; `pm_samples` is the count of those valid reads. When `pm_samples` is `0`, every PM read in the window failed and `pm25_avg`/`pm10_avg` carry `–9999.0` — an unambiguous invalid-data sentinel. `db_a_avg` is the mean over all wake cycles. `samples` documents total measurement cycles in the window.
-- `env_alert.qo` — on threshold breach only, `sync:true`, with a 30-minute cooldown per alert type. Alert types: `pm25_high`, `pm10_high`, `db_a_high`.
-
-**Routed.** Both Notefiles arrive at Notehub. From there, routes fan them out separately — `env_alert.qo` to a real-time notification channel, `env_summary.qo` to a long-term compliance archive or analytics dashboard.
-
-**What triggers alerts.**
-- `pm25_high` — PM2.5 reading at or above `pm25_alert_ug_m3` (default 35 µg/m³). A sustained reading above 35 µg/m³ is meaningful in the context of a cutting or grinding operation.
-- `pm10_high` — PM10 reading at or above `pm10_alert_ug_m3` (default 150 µg/m³). The 150 µg/m³ default is a heuristic starting point for flagging elevated coarse-particulate conditions — it is **not** equivalent to a regulatory limit or violation. The EPA 24-hour NAAQS for PM10 applies to a 24-hour average; a single 5-minute sample above that numerical value is not an exceedance. Adjust the threshold to match the dust-generation characteristics of the specific tasks on your site.
-- `db_a_high` — mean dB(A) over the 15-second window at or above `db_a_alert` (default 85 dB(A)). The 85 dB(A) default is a heuristic starting point, **not** an OSHA compliance determination. OSHA's noise action level applies to an 8-hour TWA integrated over a full shift; this device measures a 15-second mean, and the two are not directly comparable. Use the alert as an early-warning prompt to investigate, not as documentation of a regulatory exceedance.
-
-## 9. Validation and Testing
-
-**Expected steady-state cadence.** A freshly deployed unit on a quiet morning should generate two `env_summary.qo` events per hour (one every 30 minutes at default settings) and zero `env_alert.qo` events. In practice, a working construction site will generate occasional `db_a_high` alerts when heavy equipment operates near the sensor and occasional `pm25_high` alerts during active cutting, grinding, or sweeping operations.
-
-**Notehub verification.** After first power-on, the device claims itself to the project on its first cellular session (~1–2 minutes). On the Notehub dashboard, Devices page, you should see your Notecard appear with a device name (defaulting to the Notecard's serial number). Within 30 minutes, the first `env_summary.qo` Note appears in the Events table. Click on it to inspect the JSON payload — verify that `pm25_avg`, `pm10_avg`, `db_a_avg` are present and non-zero, and that `lat`/`lon` are non-zero if the device has GPS sky view. `location_valid: false` on the first few summaries is normal; it becomes `true` after the GNSS acquires a fresh fix at the current site.
-
-**First-light sanity check — two phases.**
-
-**Phase 1: USB bench (boot and I²C validation).** Connect the Notecarrier CX to a computer via USB-C, with no LiPo connected. In USB mode the Cygnet stays powered continuously; `card.attn` cannot cut the host rail because there is no ATTN-controlled LiPo path. `setup()` executes once, running one full sample cycle including the 30-second PM warm-up. Because `NotePayloadSaveAndSleep` cannot cut host power in USB-only mode, it returns without sleeping and `loop()` takes over. `loop()` repeats indefinitely, delaying between cycles and executing fresh sample cycles without any re-flashing. Open the serial monitor (115200 baud) to watch debug output. Expect to see: Notecard I²C init, `hub.set`, `note.template`, `env.get` requests, PM warm-up and sensor reads, and sound-level ADC samples. By default you will **not** see a summary `note.add` after the first cycle — the report countdown initializes to 1800 seconds (30 minutes). After approximately six cycles (~30 minutes of elapsed wall-clock time), the countdown fires and an `env_summary.qo` Note is queued. To verify the alert path: (a) Set `pm25_alert_ug_m3` to `1.0` in Notehub Fleet Environment Variables; (b) Wait for the device's next inbound sync (~30 minutes default, or force one with `{"req":"hub.sync"}` in the in-browser serial terminal); (c) The next sample cycle fires `pm25_high` immediately. Reset the variable to `35.0` afterward.
-
-**Phase 2: LiPo/solar (repeated-sampling validation).** Disconnect USB-C. Wire the Sunny Buddy LOAD to the Notecarrier CX LiPo JST and connect a charged LiPo (see §4 power chain). On Notecarrier CX, ATTN-controlled sleep is already wired to the Cygnet host power rail — no additional wiring is required. The host now wakes every `sample_interval_sec` (default 5 minutes), runs one full `setup()` sample cycle, saves state, and sleeps. Expect `env_summary.qo` to appear in Notehub approximately 30 minutes after first power-on (six 5-minute cycles). If the countdown fires but no Note appears, check `hub.status` in the blues.dev In-Browser Terminal for any cellular registration errors.
-
-**Simulating an alert.** Drop `pm25_alert_ug_m3` to `1.0` in the Fleet environment variables. The next inbound sync pulls the updated value, the next sample cycle fires `pm25_high`, and the alert should appear in Notehub within a cellular session establishment window (typically 15–60 seconds). Reset the variable to `35.0` after testing.
-
-**Using Mojo to validate power behavior.** The Notecard's published idle figure is ~8–18 µA @ 5V between cellular sessions (see the [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for the authoritative measured numbers). Active cellular transmit averages ~250 mA. The Cygnet, when powered off by `card.attn`, draws essentially zero from the LiPo rail. The SEN0232 is powered from the `V+` header pin (raw LiPo voltage), which is **not** gated by the ATTN sleep path — the sensor's op-amp circuitry draws a continuous quiescent current; measure this floor directly with the Mojo to characterize your specific build.
-
-The [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) sits inline between the Sunny Buddy `LOAD` output and the Notecarrier CX LiPo JST connector. Its LTC2959 coulomb counter (Analog Devices) accumulates the charge drawn from the LiPo regardless of whether anything is connected to its Qwiic port. To read the accumulated mAh, connect the second Qwiic cable from the PMSA003I's second STEMMA QT port to the Mojo's Qwiic port (the bench daisy-chain described in §4). With Notecard firmware v8.1.3 or later, the Notecard auto-detects the Mojo on the shared I²C bus and periodically logs energy data (including `milliamp_hours`) to `_log.qo` Notes; you can also issue `{"req":"card.power"}` from the Notecard's in-browser serial terminal for a one-shot bench reading. The firmware in this project does not call `card.power`. See the §9 extension Note for the path to include it in the summary payload.
-
-A useful bench exercise: leave the unit running on a fully charged 1200 mAh LiPo for 24 hours with no solar input and Note the Mojo tally. Only the Notecard's published figures carry vendor-backed values; the remaining phase currents should be measured rather than assumed:
-
-| Phase | Current | Duration per 5-min cycle |
-|---|---|---|
-| Notecard idle (between syncs) | ~8–18 µA @ 5V (published by Blues) | ~4 minutes |
-| Notecard cellular sync (~once per 30 minutes) | ~250 mA avg (published by Blues) | ~30 sec |
-| SEN0232 quiescent on V+ (always-on) | **Measure with Mojo** | entire cycle |
-| dB(A) sample window (SEN0232 + Cygnet) | **Measure with Mojo** | ~15 sec |
-| PM warm-up + sample (Cygnet + PMSA003I) | **Measure with Mojo** | ~40 sec |
-| GNSS re-acquisition (~once per 4 h) | **Measure with Mojo** | varies with sky view |
-
-The *shape* of the Mojo trace depends on whether the Notecarrier CX's Qwiic/3.3V rail is gated during ATTN sleep — this is carrier-implementation-specific. If the Qwiic rail **is** gated during sleep: the floor is dominated by the always-on SEN0232 quiescent draw, then a brief step for the 15-second dB(A) sample window, then a larger ~40-second step for PMSA003I warm-up and reads, and a ~30-second cellular-sync pulse every 30 minutes. If the Qwiic rail **is not** gated: the PMSA003I fan draws current continuously and raises the sleep floor — use the Mojo to distinguish these two cases, because the measured floor directly reveals whether the PM sensor is sleeping or running and determines whether the power budget and acoustic isolation story hold. Confirm the trace across at least one full 4-hour GNSS re-acquisition cycle before sizing the battery for deployment.
-
-## 10. Limitations and Next Steps
-
-This reference design is an area monitor, deliberately — not a personal dosimeter, not a regulatory silica sampler, not a complete replacement for either. The list below names the boundaries that come with that choice so a GC evaluating it can see exactly where it complements existing compliance work and where a production deployment will need to bring more. Forward-looking work that turns this into a full site monitoring program follows the limitations.
-
-### Simplified for the POC
-
-The boundaries below come with the choice to build an area monitor rather than a personal dosimeter — they show exactly where this design complements existing compliance work and where a production deployment will need to bring more.
-
-**PM sensor fan acoustic contamination.** The PMSA003I's fan runs whenever its Qwiic 3.3V supply is present. Whether that supply is cut during ATTN sleep is carrier-implementation-specific: if it is not cut, the fan runs continuously and contaminates the dB(A) channel throughout every measurement cycle. If it is cut, the fan restarts when the host wakes and has been running for the host boot + `setup()` time (~5–15 seconds) before the 15-second sound window begins. The firmware samples sound before `begin_I2C()` and the PM warm-up phase, minimizing (but not eliminating) contamination in the gated-rail case. **Before deploying, measure the enclosure's self-noise floor with the PM sensor powered and spinning, using a calibrated reference sound-level meter placed at the acoustic vent opening. If the enclosure self-noise floor exceeds 5 dB(A) above the ambient floor, the enclosure design is contaminating the dB(A) channel.** The definitive fix — regardless of carrier topology — is to add a GPIO-controlled load switch to the PMSA003I's Qwiic power line so the fan is definitively off during the entire sound-sample window. A load switch also removes any PMSA003I fan current contribution from the ATTN sleep floor, making power budgeting unambiguous.
-
-**5-minute sampling cadence can miss transient events.** The firmware wakes and samples once per `sample_interval_sec` (default 5 minutes). A high-dust burst or noise spike that begins and ends entirely between two consecutive sample windows is not captured — the sensor never sees it. During task-intensive operations such as concrete demolition, jackhammering, dry cutting, or grinding, consider reducing `sample_interval_sec` to 60–120 seconds via the Notehub environment variable to narrow the detection gap. Note that more frequent sampling reduces LiPo autonomy proportionally and increases data consumption. For tasks where any transient spike matters (e.g., a saw that only runs for 30 seconds), a wearable personal dosimeter worn by the worker is the only reliable capture method; the area monitor cannot substitute for it in those scenarios.
-
-**PM sensor ≠ direct silica monitor.** The PMSA003I is an optical particle counter measuring total PM2.5 and PM10 by mass concentration. It cannot speciate particles — it cannot tell crystalline silica from wood dust, cement dust, or urban background particulate. OSHA's silica standard (29 CFR 1926.1153) requires respirable crystalline silica sampling using NIOSH Method 7500 or equivalent, which involves filter-based personal samplers analyzed by X-ray diffraction. The PM2.5/PM10 readings from this device are a real-time proxy indicator for elevated dust conditions, **not** a substitute for OSHA-compliant silica exposure documentation. The alerts can prompt a safety officer to investigate and deploy personal monitoring; they cannot replace it.
-
-**Sound level is 15-second mean, not 8-hour TWA.** OSHA's noise standard uses TWA (time-weighted average) over a full work shift, integrated using a dosimeter that accounts for the full exposure curve including quiet and loud periods. This device reports a 15-second moving average every 5 minutes and flags when that average exceeds 85 dB(A). That is useful for identifying high-noise task windows but is not a substitute for a personal dosimeter worn by a worker throughout the shift.
-
-**SEN0232 calibration.** The DFRobot SEN0232 is rated for 3.3–5V operation, so powering it from the LiPo's V+ rail (~3.7–4.2V) is within spec. However, the sensor's published output mapping (0.6V → 30 dB, 2.6V → 130 dB) is factory-calibrated at 5V; readings at lower supply voltages may differ from a calibrated reference by a fixed offset. Use the `db_cal_offset` environment variable to null that offset against a calibrated reference sound-level meter at commissioning time.
-
-**Single-point perimeter monitoring.** One sensor reflects conditions at one location on the site. Exposure varies significantly by trade, task, and proximity to the source — a concrete saw 3 meters from the sensor reads very differently from one 20 meters away. A production deployment would use multiple sensors distributed across the site's active work zones.
-
-**Alert cooldown prevents rapid re-notification.** The 30-minute per-type alert cooldown stops alarm fatigue but also means a sustained, elevated exposure that persists for hours generates only two alerts per hour. For compliance documentation purposes, the `env_summary.qo` stream is the authoritative record, not the alert count.
-
-**PM sensor failure emits `–9999.0` as an explicit sentinel.** If the PMSA003I is not detected on I²C for an entire reporting window, `pm25_avg` and `pm10_avg` are transmitted as `–9999.0` — an unambiguous invalid-data sentinel that no downstream consumer can mistake for genuinely particulate-free air (0.0 µg/m³). The companion `pm_samples` field confirms total failure when its value is `0`. A persistent `pm_samples = 0` across multiple successive windows indicates a sensor wiring, I²C, or power fault.
-
-**GPS coordinates may be absent or stale until `location_valid` is `true`.** Two distinct scenarios can produce untrustworthy coordinates. The first is **no fix acquired yet (zero coordinates)**: if the Notecard hasn't acquired any GPS fix before the first summary fires (e.g., the enclosure is set up briefly indoors), `siteLat` and `siteLon` are both 0.0 in the payload body, and Notehub's `where_lat`/`where_lon` metadata will also be absent — both remain zero/absent until the first GNSS acquisition. The second is a **stale cached fix after redeployment**: the Notecard's GNSS hardware retains the last-known position across power cycles, so after the device is moved from one construction site to another and powered on fresh, `card.location` may immediately return the coordinates of the **previous** site — a non-zero, apparently valid position that is geographically wrong for the new site. The firmware guards against this by comparing GPS `time` timestamps: the first non-zero response after boot is treated as a baseline and `location_valid` is held `false`; only when a subsequent `card.location` response returns a newer timestamp — confirming the GNSS acquired a fresh fix at the current site — is `location_valid` set to `true` and `gpsCountdown` armed for the normal re-acquisition interval. In both cases `location_valid: false` in the Note body is the signal. Downstream consumers should **not** geo-stamp compliance records or evaluate geo-fenced alert rules on Notes where `location_valid` is `false`. In a typical outdoor deployment with clear sky view, the GNSS acquires a fresh fix within one or two sample cycles (~5–10 minutes), after which `location_valid` becomes `true` and remains `true` across sleep/wake cycles for the lifetime of the current site deployment.
-
-**Mojo is bench-validation only.** The firmware does not call `card.power` to read the Mojo's LTC2959 coulomb counter. Adding a `mojo_mah` field to the summary is a straightforward extension — include a `card.power` request in `sendSummary()` and append the returned `milliamp_hours` value to the queued Note body.
-
-### Production Next Steps
-
-Forward-looking work that turns this into a full site-monitoring program follows, roughly from site coverage to fleet maintenance and power sizing.
-
-**Multi-sensor deployments** spread coverage across the site: deploy two to four units per active site — one downwind of the primary dust-generating task (typically the saw or grinder), one at the site perimeter as a perimeter check, and one upwind near the site office as a baseline. Keep all devices on the same per-site fleet so they share site-wide threshold environment variables, and use [device-level environment variable overrides](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) for any per-unit calibration differences (e.g., `db_cal_offset`).
-
-**Shift-aware sampling cadence** matches data volume to activity: use the Notecard's on-device time (via `card.time`) to reduce sampling frequency outside of work hours (e.g., 9 PM to 5 AM) and extend it during peak-activity windows like concrete operations or demolition, reducing data volume and battery load during periods when no workers are on site.
-
-**Personal dosimeter integration** complements the area monitor: add a second, wearable Notecard + Cygnet variant as a personal dosimeter worn by a worker, with the site monitor acting as the reference station. Comparing personal vs. ambient readings lets the GC demonstrate that workers in a specific trade zone had lower exposure than the perimeter sensor suggested.
-
-**Field-upgradeable firmware** via [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) lets threshold changes, new alert types, or calibration-constant updates be pushed to the entire fleet from Notehub without a site visit.
-
-**Extended battery for winter/cloudy deployments** keeps the monitor alive through low-sun periods: after the Mojo bench exercise (§8) establishes the actual weighted-average system draw for your build, size the battery accordingly. In northern latitudes in December, available solar may be only 2–3 peak-sun hours per day. Moving to a 3000–5000 mAh LiPo pack and a higher-wattage panel (10W) provides several cloudy days of autonomy and recovers faster when the sun returns; confirm against at least one full 4-hour GNSS re-acquisition cycle in the Mojo trace before finalising battery sizing.
-
-## 11. Summary
-
-The safety officer who used to wait until the shift ended to see yesterday's PM2.5 and dB(A) numbers now gets a geo-stamped reading every 30 minutes and an alert within seconds of a threshold breach — long before the cut is finished, the saw is shut down, and the dust has settled. The GC doesn't have to negotiate WiFi access from a subcontractor, run cable to a temporary power outlet, or wait until morning to find out what yesterday's concrete cutting looked like. One Notecarrier CX, one cellular Notecard, one solar panel, and the same firmware deploys identically on every site in the GC's portfolio — fleet-per-site for threshold tuning, and when the site moves in three months, the enclosure moves with it. The summary stream archives the area-monitoring record; the alert stream is what lets the safety officer redirect workers, deploy PPE, and dispatch personal monitoring before the shift ends rather than after. That real-time visibility is the gap this device closes.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/construction-site-environmental-noise-exposure-monitor/)**
diff --git a/62-construction-site-environmental-noise-exposure-monitor/banner.png b/62-construction-site-environmental-noise-exposure-monitor/banner.png
deleted file mode 100644
index 7908906e..00000000
Binary files a/62-construction-site-environmental-noise-exposure-monitor/banner.png and /dev/null differ
diff --git a/62-construction-site-environmental-noise-exposure-monitor/diagrams/01-system-architecture.svg b/62-construction-site-environmental-noise-exposure-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index 62d60d17..00000000
--- a/62-construction-site-environmental-noise-exposure-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,73 +0,0 @@
-
diff --git a/62-construction-site-environmental-noise-exposure-monitor/diagrams/02-wiring-assembly.svg b/62-construction-site-environmental-noise-exposure-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index a2be32a9..00000000
--- a/62-construction-site-environmental-noise-exposure-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,106 +0,0 @@
-
diff --git a/62-construction-site-environmental-noise-exposure-monitor/diagrams/03-data-flow.svg b/62-construction-site-environmental-noise-exposure-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index 3e057c12..00000000
--- a/62-construction-site-environmental-noise-exposure-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,72 +0,0 @@
-
diff --git a/63-construction-equipment-anti-theft-tracker-with-immobilizer/README.md b/63-construction-equipment-anti-theft-tracker-with-immobilizer/README.md
index 52828eb6..0921985e 100644
--- a/63-construction-equipment-anti-theft-tracker-with-immobilizer/README.md
+++ b/63-construction-equipment-anti-theft-tracker-with-immobilizer/README.md
@@ -1,551 +1,5 @@
# Construction Equipment Anti-Theft Tracker with Immobilizer
-
+A cellular and satellite reference design for construction equipment fleets that reports location continuously, fires alerts on geofence breach and after-hours motion, and supports a remote ignition immobilizer.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Construction%20Equipment%20Anti-Theft%20Tracker%20with%20Immobilizer).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/construction_equipment_anti_theft/](firmware/construction_equipment_anti_theft/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [loss prevention](https://blues.com/loss-prevention/) reference design for construction equipment fleets. The system turns any skid steer, light tower, portable compressor, or generator into a hardened asset that reports its own location continuously, fires alerts on geofence breach and after-hours motion, and accepts a remotely-staged ignition immobilizer command. It runs on a self-contained cellular-plus-satellite link with a small solar-trickled battery, requiring no job-site WiFi or IT coordination.
-
-## 1. Project Overview
-
-**The problem.** Construction equipment theft is a serious, costly, and chronically underreported problem. A skid steer that costs $60,000 to replace can be driven onto a flatbed and hauled off in under ten minutes. A light tower or portable generator can be gone before the morning crew arrives. These aren't smash-and-grab events: the equipment is large, identifiable, and completely exposed on a site that often goes unwatched overnight. Law enforcement recovery rates for heavy equipment are low; the stolen unit frequently ends up at a rural stash yard, stripped, or loaded into a shipping container for export. A rural stash yard or a shipping container is exactly where cellular telemetry that depends on urban coverage goes dark.
-
-The result is an industry that leans heavily on equipment marking, insurance claims, and post-theft police reports. What's mostly missing is a device that provides real-time location during a theft in progress *and* can physically disable the equipment before it leaves the region, all without depending on an infrastructure that may not exist at the job site or at the final stash location.
-
-This project is that device. It monitors GPS location against a configurable job-site geofence, watches for unexpected after-hours motion, and accepts a remote immobilizer command from the [Blues Notehub](https://blues.com/notehub/) cloud service that stages a relay to cut the ignition circuit on the thief's next key-on attempt. This is a polled, staged proof-of-concept immobilizer rather than a continuously-held cut, with the limitations that implies (see §11). The whole stack — GPS, cellular, and satellite fallback — is contained in a single Blues Notecard for Skylo installed in a weatherproof enclosure that runs indefinitely on a LiPo battery topped off by a small solar panel.
-
-**Why Notecard.** Construction sites have no WiFi. Even if a site happens to have a hotspot, requiring site IT coordination for a security device defeats the purpose. Cellular works for the vast majority of scenarios, but a stolen skid steer doesn't stay parked in a well-covered urban area. It ends up in a rural equipment yard, a metal barn, or a shipping container at a port. That's the scenario where cellular alone fails and satellite becomes the difference between a recovered asset and a total write-off. Notecard for Skylo ([NOTE-NBGLWX](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)) handles all three radio paths — LTE-M/NB-IoT cellular, WiFi (where available), and Skylo satellite — in a single M.2 module, without any firmware branching. Global roaming and satellite failover are not optional extras for this use case; they are the core differentiators.
-
-
-
-**Deployment scenario.** A NEMA 4X weatherproof enclosure is hidden inside the equipment housing or attached to a non-visible structural member. Power is self-contained: a LiPo battery kept topped off by a small rooftop solar panel, so the tracker keeps running even after a thief disconnects the equipment's main battery. Two wires tap the ignition circuit: one reads ignition-on state via a voltage divider, one runs through the relay that can cut the starter circuit on command. Antennas are routed to externally-mounted elements that have a clear view of the sky.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The Cygnet STM32 host in the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) spends almost all of its time asleep. The whole power budget depends on it. When the configurable wake timer fires, the firmware reads ignition state from a voltage divider on the 12 V ignition line, polls the Notecard's built-in accelerometer via [`card.motion`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-motion), and pulls the most recent GPS fix via [`card.location`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location). It then runs three checks in order: is the position outside the Haversine-evaluated job-site geofence; is this the after-hours window; and is there a fresh immobilize command sitting in the inbound queue? Any heartbeat or alert Notes ride to the Notecard over I²C using the `note-arduino` request helpers, and the runtime state — geofence center, immobilizer stage, cadence parameters — is serialized to Notecard flash before sleep and rehydrated on the next wake via [`NotePayloadSaveAndSleep`](https://dev.blues.io/guides-and-tutorials/notecard-guides/attention-pin-guide/) / `NotePayloadRetrieveAfterSleep`.
-
-**Notecard responsibilities.** Notecard for Skylo is doing most of the connectivity work behind the scenes. It buffers queued [Notes](https://dev.blues.io/api-reference/glossary/#note) in on-device flash, opens a cellular or satellite session on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence, and treats any `sync:true` alert Note as an immediate uplink: the difference between an alert reaching the operator in seconds and the equipment crossing a county line. The same module also owns the GPS receiver, the accelerometer, the real-time clock, and the battery-voltage ADC, so no external sensors are needed. [Environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) flow back from Notehub on each inbound sync. A fleet manager can retune the geofence radius or the after-hours window from a browser, and the firmware automatically reissues `hub.set` and `card.location.mode` so all three Notecard cadences stay aligned with the new wake interval.
-
-**Notehub responsibilities.** [Notehub](https://notehub.io) ingests every event from every device — whether it arrived over cellular or Skylo — stores it, and applies the project's [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). Alert Notes (`alert.qo`) and tracker heartbeats (`tracker.qo`) land in separate Notefiles, so the security platform that needs alerts the moment they happen and the fleet-management store that needs position history can be served from the same device with no filter logic in between. Operators stage an `immobilize` or `release` command by posting a Note to the device's `immobilize.qi` inbound queue from the Notehub UI or via the [Notehub REST API](https://dev.blues.io/api-reference/notehub-api/). [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) push site-level geofence and after-hours defaults to every machine on a job site at once.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS IoT, Azure, GCP, Snowflake, and other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). This project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-**What you'll have when done:**
-- Device powered and claimed in Notehub, showing real GPS location
-- Real-time geofence alerts when the device moves outside a job-site boundary
-- Ability to stage an immobilizer command from the Notehub UI and verify it fires on the next ignition key-on
-- Confidence in the firmware and sensor behavior before any live equipment wiring
-
-**Fastest path to first event (bench test, no ignition circuit yet):**
-
-1. **Assemble the unit.** Insert Notecard for Skylo into Notecarrier CX M.2 slot. Connect LiPo battery to Notecarrier CX JST battery header. Route antenna cables with slack (MAIN u.FL → Skylo-certified antenna, GPS u.FL → passive GNSS patch) — finalize placement later when enclosure location is set. Mount everything in a NEMA 4X weatherproof box. Power via USB to Notecarrier CX.
-
-2. **Get your ProductUID.** Sign up at [notehub.io](https://notehub.io), create a new project, and copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) from the project settings.
-
-3. **Flash the firmware.**
- - Install [Arduino IDE](https://www.arduino.cc/en/software) or `arduino-cli`.
- - In Arduino Boards Manager, install the STM32 core and select board "Blues Cygnet".
- - In Arduino Library Manager, install "Blues Wireless Notecard".
- - Open `firmware/construction_equipment_anti_theft/construction_equipment_anti_theft.ino`.
- - Paste your ProductUID into the `PRODUCT_UID` string at the top of the sketch.
- - Flash to the Notecarrier CX.
-
- **Via arduino-cli:** The FQBN below matches `firmware/construction_equipment_anti_theft/sketch.yaml`, which the Arduino IDE picks up automatically when invoked from the sketch directory:
- ```bash
- arduino-cli compile --fqbn "STMicroelectronics:stm32:Blues:pnum=CYGNET" \
- firmware/construction_equipment_anti_theft
- arduino-cli upload --port /dev/ttyACM0 --fqbn "STMicroelectronics:stm32:Blues:pnum=CYGNET" \
- firmware/construction_equipment_anti_theft
- ```
-
-4. **Claim the device.** Power the unit and keep it powered. The Notecard connects to Skylo cellular on first boot and auto-provisions to your project. Open Notehub, navigate to your project → Devices, and confirm the Notecarrier CX appears in the device list within 30 seconds.
-
-5. **Watch the heartbeat.** Click the device in Notehub and open the Events tab. Within 1–2 minutes you should see `tracker.qo` heartbeat Notes showing current `lat`, `lon`, and battery voltage. If nothing appears, check USB power and antenna placement (clear sky view required for Skylo lock).
-
-6. **Test the geofence.** In Notehub's Fleet view, set environment variables:
- - `fence_enabled` = `1`
- - `fence_lat` = your current latitude (shown in the device event detail)
- - `fence_lon` = your current longitude
- - `fence_radius_m` = `20` (20-meter test radius)
-
- Wait 4–5 minutes for the inbound sync to deliver these variables to the device. Then physically move the device ~25 meters away and watch the Events log for a `geofence_breach` alert. Alert should appear within 1–2 wake cycles. If testing during business hours (6 AM–6 PM UTC default), the device wakes every 60 minutes — use the after-hours window (6 PM–6 AM) for faster 2-minute wake cycles.
-
-7. **Test immobilizer staging.** From the Notehub device view, use the command bar to post a Note to `immobilize.qi`:
- ```json
- {"cmd":"immobilize"}
- ```
- Watch the Events log for `immobilize_armed` — that Note confirms the device received and staged the command. Now simulate a key-on edge: connect the Cygnet's A2 GPIO to GND (ignition OFF state), let it sit for one full wake cycle, then briefly short A2 to 3.3V (ignition ON). On the next wake after the OFF→ON transition, the relay driver pin A1 should pulse HIGH and `ignition_on_immobilized` should appear in the Events log. This confirms the immobilizer path is functional before live wiring.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "tracker.qo",
- "body": {
- "lat": 37.3387,
- "lon": -121.8853,
- "loc_valid": 1,
- "ignition": 0,
- "moving": 0,
- "immobilized": 0,
- "bat_v": 3.92,
- "fence_ok": 1,
- "fix_age_s": 47.0
- }
-}
-```
-
-## 4. Hardware Requirements
-
-
-
-**12 V systems only.** This reference design — the voltage-divider ratios, relay coil rating, and all wiring guidance — is engineered for 12 V electrical systems. Most compact construction equipment (skid steers, portable generators, light towers) ships with a 12 V system, but larger machines (heavy excavators, some European platforms) use 24 V. With the 33 kΩ / 10 kΩ ignition-sense divider shown here, a 24 V ignition rail would put approximately 5.6 V on the Cygnet A2 GPIO — exceeding the 3.3 V limit, and the 12 V relay coil would overheat. **Verify your equipment's system voltage before proceeding.** Adapting to 24 V requires different divider values (e.g. 68 kΩ high-side + 10 kΩ low-side → 24 V × 10/78 ≈ 3.1 V, within the GPIO limit) and a 24 V–rated relay coil.
-
-
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/)) | 1 | Compact carrier with embedded Cygnet STM32 host MCU — no separate Swan or Feather needed. ATTN pin is wired to control the Cygnet's power rail, enabling deep sleep via `card.attn`. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)) | 1 | All-in-one LTE-M/NB-IoT/GPRS + WiFi + Skylo satellite in a single M.2 module. Automatic cellular-to-satellite failover with no firmware changes. Ships with its Skylo-certified `MAIN` cellular/satellite antenna (see antenna row below). Includes 500 MB cellular data and 10 KB/month Skylo data. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/mojo-datasheet/)) | 1 | Bench-only coulomb counter for energy validation during commissioning. Placed inline on the +VBAT rail to measure current draw across sleep, active, and transmit phases. Not deployed in the field (see §11). |
-| 3.7 V LiPo battery, 2000 mAh, JST-PH 2.0 mm (e.g. [Adafruit 2011](https://www.adafruit.com/product/2011), or equivalent 2000–4000 mAh cell) | 1 | Primary energy storage. Size capacity against expected solar availability and desired dark-sky reserve; see §9 for a phase-by-phase current-draw breakdown to inform your sizing. |
-| 6 V, 10 W solar panel, ~110×70 mm (e.g. [Voltaic Systems P110](https://voltaicsystems.com/p110/)) | 1 | Trickle-charges the LiPo during daylight hours. Construction equipment typically lives outdoors; the 10 W panel provides ample headroom even for sites with limited direct sun or extended overcast periods. |
-| Solar LiPo charger module, 3.7 V LiPo output (e.g. [SparkFun Sunny Buddy PRT-12885](https://www.sparkfun.com/products/12885), or equivalent MPPT/CV-mode charger) | 1 | Sits between the solar panel and the LiPo, protecting the battery from overcharge. The Notecarrier CX provides a LiPo JST input; the charger output connects to it. Verify the charger's output connector matches the Notecarrier CX's battery header. |
-| BSS138 logic-level N-channel MOSFET, SOT-23 (e.g. [Nexperia BSS138](https://assets.nexperia.com/documents/data-sheet/BSS138.pdf) or equivalent with R_DS(on) ≤ 3.5 Ω at V_GS = 2.5 V and I_D ≥ 300 mA) | 1 | Low-side relay coil driver. The correct selection criterion for a 3.3 V logic-level gate drive on a security-critical path is **R_DS(on) at V_GS = 2.5 V**, not threshold voltage. The Nexperia BSS138 datasheet specifies R_DS(on) max 3.5 Ω at V_GS = 2.5 V, I_D = 100 mA. At the relay coil current (~150–175 mA for a typical ISO 280 coil), worst-case V_DS ≤ 175 mA × 3.5 Ω ≈ 0.61 V and P_D ≈ 0.11 W — within the SOT-23 thermal rating. The relay coil sees ≥ 11.4 V of the 12 V supply, well above its pull-in threshold. Drain to relay coil −; source to GND. The 10 kΩ gate-to-source pulldown keeps the MOSFET off when the GPIO is high-impedance during reset. |
-| Automotive SPDT relay, 12 V coil, 30 A contacts — ISO 280 mini relay (e.g. Bosch 0 332 019 150 or equivalent Tyco/TE V23134-J52-X255) | 1 | Wired in series with the equipment's **starter solenoid enable or ignition/run-start control circuit** — the low-current signal wire that authorizes cranking, not the high-current cable that feeds the starter motor itself. When the coil is energized, the normally-closed contact opens, preventing the engine from cranking. Available at any automotive supplier or electronics distributor. |
-| Inline automotive fuse holder with 1 A or 2 A mini (ATC/ATO blade) fuse | 1 | Placed on the wire feeding the relay coil + terminal from the 12 V battery or ignition tap — as close to the power source as practicable. A typical ISO 280 coil draws ~150–175 mA; a 1 A fuse provides ~6× margin over continuous coil current while protecting the added harness from a short-circuit fault. Do not rely on the equipment's upstream fusing — any newly-added conductor tied to battery or ignition power should carry its own fuse near the source. |
-| 1N4007 diode | 1 | Flyback protection across the relay coil. Essential for protecting the BSS138 MOSFET drain from the inductive spike when the coil de-energizes. |
-| 10 kΩ resistor, ¼ W | 1 | Gate-to-source pulldown on the BSS138. Ensures the MOSFET stays off if the Cygnet GPIO is high-impedance during reset or power-on. |
-| 33 kΩ + 10 kΩ resistors, ¼ W, 1% tolerance | 2 | Resistive voltage divider for ignition sense: 33 kΩ high-side (to 12 V rail) + 10 kΩ low-side (to GND) → 12 V × 10 / (33 + 10) ≈ 2.79 V at A2 — safely within the Cygnet's 3.3 V GPIO absolute maximum. 1% tolerance for a consistent threshold. **POC-only front end** — this bare divider provides no protection against automotive load-dump or starter transients; see §5 and §11 for production protection guidance. |
-| NEMA 4X weatherproof enclosure, ~150×100×75 mm (e.g. [Polycase WC-31](https://www.polycase.com/wc-31) or Hammond 1554N2GYCL) | 1 | Rated for outdoor hose-down environments. Mount inside the equipment housing where it is not visible from outside. |
-| Skylo-certified cellular/satellite antenna — **use the antenna included in the NOTE-NBGLWX kit** | 1 | Connects to the `MAIN` u.FL on Notecard for Skylo. The NOTE-NBGLWX is certified on Skylo's network exclusively with the included antenna; substituting a different antenna voids Skylo network certification and may result in network blocking. See the [antenna guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/) for placement requirements. |
-| Passive GNSS antenna, GPS L1 1575 MHz, u.FL connector, outdoor-rated (e.g. [Taoglas FXP611 (Cloud)](https://www.taoglas.com/product/cloud-fxp611-gps-glonass-compass-flexible-pcb-2/) flexible PCB GPS/GLONASS antenna with u.FL lead, or equivalent passive GPS patch) | 1 | Connects to the `GPS` u.FL on Notecard for Skylo. Must be a passive antenna (the `GPS` port does not supply bias voltage). Mount outdoors with clear sky view alongside the MAIN antenna. |
-
-All Blues hardware ships with an active SIM including 500 MB of cellular data and 10 years of service — no activation fee, no monthly commitment.
-
-
-
-**Important satellite Note.** Skylo's constellation sits in geostationary orbit (GEO). From the northern hemisphere, the antenna needs an unobstructed view toward the southern sky to lock onto the satellites. A metal equipment cab that blocks the southern horizon will prevent satellite connectivity. Mount the antenna on the roof of the enclosure and orient it accordingly. See the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) for detailed antenna placement guidance.
-
-
-
-## 5. Wiring and Assembly
-
-
-
-**Safety.** The ignition and starter circuits on heavy equipment carry significant inrush current and may involve safety-critical functions. All wiring must be performed by qualified personnel following the equipment manufacturer's service manual and applicable electrical codes. The relay wiring described here is a reference example — confirm the correct interrupt point for your specific machine (skid steer, generator, light tower, etc.) before installation. Never interrupt circuits that control active running loads (fuel injection, hydraulics) while the engine is running.
-
-
-
-The whole assembly has to disappear inside the equipment chassis — behind a panel, under a seat, in an equipment bay where a thief won't find it — so the build is small and the antennas are the only parts that need to see the sky. Notecard for Skylo seats into the Notecarrier CX's M.2 slot; route the **`MAIN` u.FL** lead to the Skylo-certified antenna that came with the kit and the **`GPS` u.FL** lead to the separate passive GNSS antenna. Leave both leads slack until the final enclosure mounting location is set — antenna placement and clear sky view matter significantly for Skylo lock.
-
-During bench validation the Mojo coulomb counter splices inline on the +VBAT wire to measure phase-by-phase current draw; see the [Mojo datasheet](https://dev.blues.io/datasheets/mojo-datasheet/) for wiring. It comes out before the unit ships to the field (or stays in circuit only if you add the Qwiic telemetry extension — see §11).
-
-
-
-**12 V systems only.** Confirm the equipment's system voltage is 12 V before connecting the ignition-sense divider or relay coil. On a 24 V machine the divider would exceed the Cygnet GPIO limit and the relay coil would overheat. See the §4 Note for 24 V adaptation guidance.
-
-
-
-**Pin-by-pin wiring:**
-
-**Power chain (follow this order):**
-1. **Solar panel (+/−)** → charger module solar/panel input terminals.
-2. **LiPo battery JST** → charger module battery output connector (the dedicated battery terminal on the charger, not its load output). The charger regulates charging from the solar panel and protects the cell from overcharge.
-3. **Charger load/output (+)** → (during bench validation: Mojo `BAT` +, then Mojo `LOAD` + to next step; otherwise directly to next step) → **Notecarrier CX battery JST connector** (the `BATT` / `+VBAT` header). This is the supply rail for the Notecarrier CX and the embedded Cygnet. Charger load/output (−) and Mojo GND to common GND.
-
-Verify that your charger module's output connector pitch and polarity match the Notecarrier CX battery JST-PH 2.0 mm header before connecting. Some charger modules ship with reversed polarity JST connectors.
-
-**Signal wiring:**
-- **A1 (relay driver GPIO)** → BSS138 gate. BSS138 source to GND. 10 kΩ gate-to-source pulldown resistor (from BSS138 gate to GND) — keeps the MOSFET off when the GPIO is high-impedance during reset. BSS138 drain to relay coil pin 85 (coil `−`). **Inline 1–2 A automotive fuse** on the wire between the 12 V supply tap and relay coil `+` (pin 86) — place the fuse as close to the 12 V source as practicable. 1N4007 cathode to pin 86 (+12 V), anode to pin 85 (MOSFET drain/coil `−`) — across the coil, clamping the inductive spike when the coil de-energizes. At V_GS = 3.3 V the BSS138 is operating within its R_DS(on)-characterized region (max 3.5 Ω at V_GS = 2.5 V per the Nexperia datasheet), so the relay coil is reliably energized on every wake that asserts the driver pin.
-- **A2 (ignition sense)** → junction node of voltage divider. **33 kΩ high-side resistor** from the equipment's 12 V ignition rail to the A2 junction node. **10 kΩ low-side resistor** from the A2 junction node to GND. This divides 12 V down to approximately 2.79 V at the Cygnet GPIO — 12 V × 10 / (33 + 10) ≈ 2.79 V, safely within the 3.3 V absolute maximum.
-
- > **Ignition-sense front end — POC only.** The bare resistive divider provides no protection against the automotive transients that appear on ignition and battery lines in real equipment: load-dump events on generator-set systems can produce transients of 40–200 V above nominal; starter motor switching causes fast inductive spikes on the 12 V rail. A production design should add at least a TVS diode (e.g. 15 V unidirectional TVS from the 12 V rail to GND, placed before the divider high-side resistor) and an RC low-pass filter (e.g. 1 kΩ series + 100 nF to GND at the A2 node) to clamp transients and filter noise before they reach the Cygnet GPIO. An optically-isolated or fully-automotive-rated sensing front end is the correct long-term solution for production deployments.
-- **Relay switched contacts** → NC (normally-closed) contact (pins 87a and 30) wired in series with the **starter solenoid enable or ignition/run-start control circuit** — the low-current signal wire that authorizes cranking. With coil de-energized, current flows normally; coil energized opens the circuit. **Do not wire the relay in the high-current starter motor power cable** (the heavy-gauge cable running between the battery positive terminal and the starter motor terminal); that cable carries hundreds of amperes during cranking and is not the correct intercept point for this relay. Consult the equipment service manual to identify the correct low-current control wire.
-- **SDA / SCL** → Notecard for Skylo communicates internally on the Notecarrier CX's onboard I²C bus; no external wiring needed.
-- **GND** → equipment chassis ground for the ignition sense divider and relay coil return. Keep this common with the Notecarrier CX GND.
-
-**Enclosure and antenna mounting.** Mount the enclosure in a concealed, non-obvious location inside the equipment chassis — behind a panel, under a seat, inside an equipment bay. Route the antenna cable through a weatherproof grommet. For the best Skylo link budget, the antenna patch should face toward the sky with as much southern hemisphere clearance as possible from the installation latitude.
-
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/construction_equipment_anti_theft/construction_equipment_anti_theft.ino` as `PRODUCT_UID`.
-2. **Claim the Notecard.** Power the unit. On first cellular connection the Notecard auto-provisions to your project. Verify the device appears in the Notehub device list.
-3. **Create a Fleet per job site.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. One fleet per active job site is a natural boundary — all equipment on site shares the same geofence center and after-hours window via fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/), with per-device overrides available when a specific machine has unusual requirements. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can automate fleet membership based on location data.
-4. **Set environment variables.** All variables are optional; firmware defaults are shown. Variables are pulled on each device wake via a delta check — no firmware re-flash required to retune thresholds. When cadence variables change, the firmware automatically reissues both `hub.set` (outbound and inbound session windows) and `card.location.mode` (GNSS acquisition cadence) to keep all three Notecard cadences in sync with the updated host wake interval.
-
- | Variable | Default | Range / Notes | Purpose |
- |---|---|---|---|
- | `fence_enabled` | *(not set)* | `1` to enable | **Recommended commissioning flag.** When set to `1`, applies `fence_lat` and `fence_lon` to the geofence regardless of their numeric values. Preferred over the legacy non-zero-check fallback because it supports fences centered exactly on latitude 0° or longitude 0°. If `fence_enabled` is absent, the firmware falls back to checking that `fence_lat` and `fence_lon` are both non-zero. |
- | `fence_lat` | *(first GPS fix)* | Decimal degrees | Latitude of job-site geofence center. Set `fence_enabled=1` alongside this value (see above). If neither `fence_enabled` nor a non-zero `fence_lat` is present, firmware anchors the geofence at the first valid GPS fix after cold boot. |
- | `fence_lon` | *(first GPS fix)* | Decimal degrees | Longitude of job-site geofence center. Set `fence_enabled=1` to apply this value regardless of magnitude — the legacy fallback silently ignores a `fence_lon` of exactly `0.0` (the prime meridian). |
- | `fence_radius_m` | `200` | > 0 | Geofence radius in meters. 200 m works well for a single job site; expand for large sites or reduce for tighter perimeter security. |
- | `after_hours_start` | `18` | 1–23 | UTC hour when after-hours monitoring begins. `0` is treated as "unset" (firmware keeps existing value). Adjust for the UTC offset of the deployment region. |
- | `after_hours_end` | `6` | 0–23 | UTC hour when after-hours monitoring ends. `0` (midnight) is valid. |
- | `heartbeat_stopped_min` | `60` | > 0 | Minutes between host wake cycles and queued heartbeat Notes when equipment is stationary. Also controls the daytime inbound cadence (the Notecard polls Notehub once per this interval while parked. See `inbound_min`). The worst-case geofence-detection latency during business hours equals this value; reduce it for faster detection at the cost of more wake cycles. |
- | `heartbeat_moving_min` | `5` | > 0 | Minutes between **queued** heartbeat Notes when the Notecard accelerometer reports motion. Also sets the GNSS acquisition cadence (`card.location.mode seconds`) so the Notecard re-acquires a fresh position fix at this interval while moving. Note that heartbeat records accumulate in local flash and are batch-transmitted on the next outbound sync session; they are not individually transmitted at this cadence. Real-time pursuit location updates come from `alert.qo` Notes (`sync:true`), not from the heartbeat queue. |
- | `heartbeat_afterhours_min` | `2` | > 0 | Minutes between host wake cycles during the after-hours window. Controls alert-detection latency — the Cygnet wakes this often to scan for motion and check the command queue. Heartbeats still queue at the stationary/moving rate regardless of this value. Does **not** control the Notecard inbound cadence; see `inbound_min`. |
- | `inbound_min` | `4` | 1–60 | Short inbound interval used **after-hours and when moving** — how often the Notecard polls Notehub for queued commands and environment-variable updates during active-monitoring windows. During daytime stopped operation the firmware automatically uses `heartbeat_stopped_min` as the inbound interval instead, so inbound sessions match host wakes and no idle cellular connections are initiated while the equipment is parked on the job site. The inbound poll is Notecard-driven and does **not** wake the host MCU. |
- | `outbound_min` | `240` | > 0 | Minutes between Notecard outbound batch sessions — how often queued heartbeat Notes are transmitted to Notehub. Intentionally longer than `heartbeat_stopped_min` so multiple heartbeats accumulate per outbound session, reducing total cellular radio time without degrading detection latency. Alert Notes (`sync:true`) bypass this window and transmit immediately. |
- | `alert_cooldown_min` | `5` | > 0 | Minimum minutes between repeated alerts of the same type. Each alert type (`geofence_breach`, `motion_after_hours`, `cmd_retrieve_failed`) has its own independent cooldown timer, so one alert type cannot suppress the other. On the Skylo satellite link, consider raising this to 15–30 minutes to protect the 10 KB/month data budget during a sustained geofence-breach event. |
-
-5. **Issue an immobilize command.** From the Notehub UI (or via the [Notehub REST API](https://dev.blues.io/api-reference/notehub-api/)), add a Note to the device's `immobilize.qi` inbound queue with body `{"cmd":"immobilize"}`. Command delivery has two **sequential** latency stages: (1) the Notecard pulls the inbound Note on its next `inbound` sync session, and (2) the host Cygnet processes the command only when it next wakes and runs `setup()`. Under the default cadence, combined worst-case end-to-end latency from posting the command to the `immobilize_armed` acknowledgment appearing in Notehub is at most **~6 minutes** during the after-hours window (≤ 4 minutes for the Notecard inbound sync, then ≤ 2 minutes until the next Cygnet wake), **~9 minutes** while moving (4 + 5 minutes), and just under **2 hours** while stationary during business hours (up to 60 minutes for the Notecard daytime inbound sync + up to 60 minutes until the next Cygnet wake). The daytime inbound automatically extends to match `heartbeat_stopped_min` (default 60 minutes) to avoid idle cellular sessions; if near-real-time command delivery is required around the clock, reduce `heartbeat_stopped_min` or add a motion-triggered wake (see §11 Production next steps). To cancel before the relay fires, send `{"cmd":"release"}`.
-
- **Queue semantics — last command wins.** The firmware drains the entire `immobilize.qi` queue on each wake: it calls `note.get` in a loop until the queue is empty, applying each command in order. The final command in the queue takes effect. If you post `immobilize` and immediately follow it with `release`, the `release` will win on the same wake cycle. Keep at most one outstanding command in the queue at any time; do not post a new command until either the `immobilize_armed` alert (confirming a staged immobilize) or the `release_confirmed` alert (confirming the immobilize was cleared) appears in Notehub. Both alerts are emitted once — on the exact wake cycle that processes the corresponding command. This is a **staged, edge-triggered** POC immobilizer — the relay is not asserted until the device wakes and detects an OFF→ON ignition edge while the command is staged (one prior wake observed OFF, the firing wake observes ON). The relay deliberately does not fire while the engine is already running.
-
- > **Important:** For a remote immobilizer this is a security-sensitive control path. Restrict `immobilize.qi` write access to tightly scoped, audited Notehub API credentials — do not use a project-wide admin token. Anyone who can post to `immobilize.qi` can cut ignition on the equipment.
-
-6. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `alert.qo` (real-time delivery to a security or on-call platform) and a second for `tracker.qo` (position history to a fleet-management or asset-tracking store). Keeping the two Notefiles separate at the source lets you route them to different systems at different urgency without filter logic in the route itself.
-
-## 7. Firmware Design
-
-Every wake runs the same compact cycle: sense, evaluate, queue any Notes, save state, sleep. The application logic and the `setup()` / `loop()` entry points live in the main sketch [`firmware/construction_equipment_anti_theft/construction_equipment_anti_theft.ino`](firmware/construction_equipment_anti_theft/construction_equipment_anti_theft.ino); all Notecard interaction is encapsulated in [`construction_equipment_anti_theft_helpers.h`](firmware/construction_equipment_anti_theft/construction_equipment_anti_theft_helpers.h) and [`construction_equipment_anti_theft_helpers.cpp`](firmware/construction_equipment_anti_theft/construction_equipment_anti_theft_helpers.cpp).
-
-**Dependencies:**
-- Arduino Core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)) — target board: Blues Cygnet (available in the Arduino Boards Manager).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (`note-arduino` library). Install via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`.
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard configuration: `hub.set`, GPS mode, motion mode, templates (idempotent, runs every wake) | `ensureConfigured()` |
-| Compact Note templates for satellite | `defineTemplates()` |
-| Env-variable delta pull | `fetchEnvOverrides()` |
-| Sync hub inbound/outbound and GNSS cadences to wake-state context | `applyHubCadence()` |
-| Drain inbound command queue (last command wins) | `checkAndHandleCommand()` |
-| Ignition state, motion state, GPS fix, battery voltage | `getIgnitionState()`, `getIsMoving()`, `getLocation()`, `getBatteryVoltage()` |
-| Geofence evaluation (Haversine) | `haversineDistanceM()` |
-| After-hours window evaluation (UTC) | `isAfterHours()` |
-| Heartbeat and alert Note emission | `sendHeartbeat()`, `sendAlert()` |
-| Relay assertion / release | `assertRelay()`, `releaseRelay()` |
-| State persistence across sleep | `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### Sensor reading strategy
-
-**Ignition sense.** A voltage divider on the equipment's 12 V ignition rail presents ~2.79 V to the Cygnet's A2 pin when the ignition key is ON — within the 3.3 V safe limit. The high-side resistor (33 kΩ, to the 12 V rail) and low-side resistor (10 kΩ, to GND) set the divider ratio: 12 V × 10 / (33 + 10) ≈ 2.79 V. On each wake the Cygnet reads A2 with `analogRead()` at 12-bit resolution, averaging 4 samples to filter automotive line noise and battery-sag transients during engine crank. State changes are governed by hysteresis thresholds: the averaged reading must exceed ~2.50 V (3103 counts on a 12-bit / 3.3 V ADC) to declare ignition ON, and must fall below ~2.00 V (2482 counts) to declare ignition OFF. Within the ~0.5 V hysteresis band the firmware holds the previous state, preventing chatter at the logic boundary and spanning the typical battery-sag floor (~10.5 V → ~2.44 V at the divider) during cold cranks.
-
-**Motion.** Notecard for Skylo's built-in accelerometer runs continuously in low-power mode. `card.motion.mode` configures it for 25Hz, ±4G sensitivity with a 60-second bucket. The firmware calls `card.motion` on each wake; when the bucket contains at least 3 events, the Notecard reports `"mode":"moving"`. This approach detects both equipment being driven away (sustained multi-axis motion) and equipment being loaded onto a trailer (tilt, vibration).
-
-**GPS.** `card.location.mode` is set to `periodic` with a motion threshold of 4 events, so the Notecard only spins up the GNSS module when the accelerometer confirms the unit is actually moving — avoiding the multi-second GNSS warm-up penalty on every stationary wake. The firmware calls `card.location` to read the most recent fix; the Notecard caches the last valid location so the call returns quickly even when GNSS is not active.
-
-
-
-**Cached-fix latency.** Geofence and alert decisions are made on the most recently cached GNSS fix, which may not reflect the device's current position. Fix age is bounded by the GNSS acquisition cadence (`card.location.mode seconds`, default 5 minutes, tunable via `heartbeat_moving_min`). During the after-hours window the host wakes every 2 minutes but GNSS updates at 5 minutes, so two or three consecutive wakes may evaluate geofence against the same cached fix. The firmware captures the `time` field from `card.location` (the epoch of the cached fix) and computes `fix_age_s = now − fix_time` only when `card.time` returns a valid non-zero epoch. When `card.time` is unavailable, for example, on first boot before the Notecard has completed a Notehub session — `fix_age_s` is emitted as **`-1`** (sentinel: "age unknown") rather than `0`, so downstream consumers can distinguish a genuinely fresh fix from a case where the age simply cannot be determined. A value of `0` or greater is a real elapsed-seconds count. `fix_age_s` is included in every outbound Note — both `tracker.qo` heartbeats and `alert.qo` events.
-
-
-
-**Battery voltage.** `card.voltage` returns the LiPo rail voltage directly from the Notecard's internal ADC. Included in every Note for battery state-of-charge trending.
-
-### Event payload design
-
-Both Notefiles use [`compact` Note templates](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) with a `port` number. Compact templates are **required** for Notecard for Skylo: they store Notes as fixed-length binary records rather than free-form JSON, shrinking per-Note wire size by 3–5×.
-
-Compact record sizes are derived directly from the template field types declared in `defineTemplates()`:
-
-- **`tracker.qo`** — 4 floats (`lat`, `lon`, `bat_v`, `fix_age_s`) × 4 bytes + 5 int16 fields (`loc_valid`, `ignition`, `moving`, `immobilized`, `fence_ok`) × 2 bytes = **26 bytes** per heartbeat record.
-- **`alert.qo`** — 4 floats (`lat`, `lon`, `bat_v`, `fix_age_s`) × 4 bytes + 3 int16 fields (`loc_valid`, `ignition`, `immobilized`) × 2 bytes + 1 string field (`alert`, 24-char sample) at 25 bytes (1-byte length prefix + 24 data bytes) = **47 bytes** per alert record.
-
-At the 10 KB/month Skylo data budget (10,240 bytes), payload-only capacity is roughly **390 heartbeat-sized Notes** (10,240 ÷ 26 bytes) or **215 alert-sized Notes** (10,240 ÷ 47 bytes) per month. Protocol and session framing overhead reduce actual delivered counts below these payload-only maxima — validate against Notehub usage metrics and field measurements rather than relying on payload-only arithmetic. See §11 for practical budget guidance.
-
-Example heartbeat (`tracker.qo`):
-
-```json
-{
- "file": "tracker.qo",
- "body": {
- "lat": 37.3387,
- "lon": -121.8853,
- "loc_valid": 1,
- "ignition": 0,
- "moving": 0,
- "immobilized": 0,
- "bat_v": 3.92,
- "fence_ok": 1,
- "fix_age_s": 47.0
- }
-}
-```
-
-`fix_age_s` is the age of the cached GNSS fix at the moment the Note was queued, in seconds. **`-1.0`** means `card.time` was unavailable when the Note was queued — the true fix age cannot be determined; do not treat this as a freshness indicator. A value of `0.0` or greater is a real elapsed-seconds count: well below `heartbeat_moving_min × 60` (default 300 seconds) indicates a fresh fix; approaching or exceeding that threshold means the geofence and alert decisions on this wake used a cached position from the previous GNSS acquisition cycle.
-
-Example alert (`alert.qo`), sent with `sync:true`:
-
-```json
-{
- "file": "alert.qo",
- "body": {
- "alert": "geofence_breach",
- "lat": 37.3521,
- "lon": -121.9204,
- "loc_valid": 1,
- "ignition": 1,
- "immobilized": 0,
- "bat_v": 3.87,
- "fix_age_s": 112.0
- },
- "sync": true
-}
-```
-
-Alert types:
-
-| `alert` value | Meaning |
-|---|---|
-| `geofence_breach` | Equipment GPS fix is outside the configured fence radius |
-| `motion_after_hours` | Accelerometer detects motion during after-hours window, ignition confirmed OFF |
-| `ignition_on_immobilized` | Immobilize was staged; relay asserted on an OFF→ON ignition edge (not on a level read of ignition-ON) |
-| `immobilize_armed` | Acknowledgment Note confirming the immobilize command was received and staged |
-| `release_confirmed` | Acknowledgment Note confirming a `release` command cleared the staged or active immobilize; relay de-energized |
-| `cmd_retrieve_failed` | Diagnostic: `note.get` on `immobilize.qi` exhausted all retries — the command path is unhealthy. Rate-limited by `alert_cooldown_min` (default 5 minutes) so a persistent I²C or Notecard fault does not generate repeated `sync:true` traffic. Investigate I²C wiring and Notecard firmware if this alert fires repeatedly. |
-
-### Low-power strategy
-
-The Cygnet host and the Notecard are both put to sleep between sensing cycles. At the end of each wake, the firmware calls `NotePayloadSaveAndSleep`, which serializes the `AppState` struct to Notecard flash as a base64 payload and then issues [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) with `mode:sleep`, physically cutting the Cygnet's power via the Notecarrier CX's ATTN-controlled power rail. The Notecard itself enters its own [low-power idle state](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) (~8–18 µA @ 5V) between cellular sync windows. After `sleep_sec` seconds, the ATTN pin goes HIGH and the Cygnet's power is restored; `setup()` runs fresh and `NotePayloadRetrieveAfterSleep` rehydrates the state struct.
-
-The sleep duration adapts to context: 2 minutes during after-hours (fast theft detection), 5 minutes while moving (active tracking), 1 hour during business hours at rest (background monitoring, and worst-case geofence-detection latency while parked). The after-hours wake interval is intentionally shorter than the heartbeat emission interval: the device wakes every 2 minutes to check for alerts and inbound commands, but heartbeat Notes queue at the stationary or moving rate regardless of time of day. This keeps the overnight Note volume consistent with daytime operation while still catching a theft event within the scan window. **Sampling/queuing cadence and transmission cadence are always distinct.** Heartbeat Notes are queued by the host into the Notecard's local flash, then transmitted to Notehub on the next outbound sync session, which fires at the `hub.set outbound` interval (set by `outbound_min`, default 240 minutes). A 5-minute moving sample rate means the Notecard accumulates a heartbeat every 5 minutes while in motion, but those records are batch-transmitted on the next outbound session unless an alert happens to force a `sync:true` flush first. Only `alert.qo` Notes (with `sync:true`) bypass the outbound window and trigger an immediate uplink sync. Separately, the Notecard independently manages its own **inbound sync sessions**: these are cellular connections the Notecard initiates on its own schedule to pull queued commands and updated environment variables from Notehub. The inbound cadence is context-aware: when the equipment is parked and daytime, the interval matches `heartbeat_stopped_min` (default 60 minutes, set via `hub.set inbound`); when after-hours or moving, it shortens to `inbound_min` (default 4 minutes). These inbound sessions are Notecard-driven and occur regardless of whether the Cygnet host is currently awake. The host's 2-minute after-hours wake cycle and the Notecard's inbound cellular cadence are independent of each other and tuned via separate parameters (`heartbeat_afterhours_min` and `inbound_min`, respectively), so daytime inbound sessions match the host wake cadence rather than running dozens of times per hour against a parked asset. Both cadences must be accounted for when sizing the energy budget (see §9).
-
-**Env-var cadence retuning scope.** When an operator changes any cadence variable (`heartbeat_moving_min`, `heartbeat_stopped_min`, `heartbeat_afterhours_min`, `inbound_min`, or `outbound_min`) via Notehub, the firmware detects the change via the `env.get` delta check and calls `applyHubCadence()`, which reissues `hub.set` (outbound and inbound windows) and `card.location.mode` (GNSS acquisition seconds, set to `heartbeat_moving_s`). `applyHubCadence()` is also called immediately when the motion or after-hours context transitions, so the Notecard's inbound window switches between the short active-monitoring interval (`inbound_min`) and the long parked interval (`heartbeat_stopped_min`) without waiting for a future env-var change. The outbound window uses `outbound_s` (set by `outbound_min`, default 240 minutes), decoupled from the host wake interval. Env-var changes do not affect `card.motion.mode` (accelerometer sensitivity and bucket size); those are set at cold boot only and require a firmware update to change.
-
-The GPS module is motion-gated at the Notecard level: it only activates after sustained accelerometer activity, keeping the GNSS radio silent during the many hours per day the equipment is parked and not moving.
-
-### Retry and error handling
-
-- The very first Notecard transaction — a `card.version` request issued immediately after `notecard.begin()` in `setup()` — uses `notecard.sendRequestWithRetry(req, 5)` (5-second window) to absorb the known cold-boot I²C race where the Cygnet host may power up before the Notecard is ready to ACK. This ensures I²C is confirmed live before `NotePayloadRetrieveAfterSleep()` runs. Without this guard, a transient startup race causes the state-restore call to silently return false, the firmware treats the wake as a cold boot, zeros `g_state`, and loses all staged immobilizer / alert-cooldown / fence state from the previous cycle. The subsequent `hub.set` in `ensureConfigured()` also uses `sendRequestWithRetry()` to handle an independent Notecard reset that occurs after the host has already booted.
-- All `requestAndResponse` calls check for a `NULL` response before accessing the returned JSON. Calls to `fetchEnvOverrides`, `checkAndHandleCommand`, and `getLocation` also check for a non-null `"err"` field. Calls to `getIsMoving`, `getBatteryVoltage`, and `getEpochTime` do the same. Sensor calls that fail return safe defaults (zero for GPS coordinates, `false` for motion/ignition) rather than propagating bad data into alerts or Notes.
-- `note.get` on `immobilize.qi` returns `err` when no Note is queued — the firmware silently ignores this and continues the wake cycle.
-- `env.get` with the `time` delta parameter still performs the I²C request every wake, but when no environment variables have changed since the last pull the Notecard returns an `err` response rather than the full env-var payload — the firmware detects this and skips the overhead of re-parsing and re-applying unchanged values.
-- When `env.get` does return updated values, the firmware immediately reissues `hub.set` via `applyHubCadence()` so the Notecard's outbound and inbound session windows reflect any changed heartbeat intervals. If this reissue is skipped (e.g., Notecard not ready), the cadence remains at the last successfully applied value.
-- Alert de-duplication uses separate per-type cooldown timestamps (`last_geofence_alert_s` and `last_motion_alert_s`), so a geofence breach cooldown cannot suppress a concurrent after-hours motion alert and vice versa.
-
-### Key code snippet 1: compact Note template definition
-
-The `format:"compact"` and `port:50` arguments are required for Notecard for Skylo. Without them, Notes destined for the satellite path may be dropped. The numeric type hints (`14.1` = 4-byte float, `12` = 2-byte signed int) define the binary record layout.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "tracker.qo");
-JAddStringToObject(req, "format", "compact");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "lat", 14.1); // 4-byte float
-JAddNumberToObject(body, "lon", 14.1); // 4-byte float
-JAddNumberToObject(body, "loc_valid", 12); // 2-byte int16 — 1 = real fix, 0 = no fix
-JAddNumberToObject(body, "ignition", 12); // 2-byte int16
-JAddNumberToObject(body, "moving", 12); // 2-byte int16
-JAddNumberToObject(body, "immobilized", 12); // 2-byte int16
-JAddNumberToObject(body, "bat_v", 14.1); // 4-byte float
-JAddNumberToObject(body, "fence_ok", 12); // 2-byte int16 — 1 = in-fence, 0 = breach, -1 = unknown
-JAddNumberToObject(body, "fix_age_s", 14.1); // 4-byte float — seconds since last GNSS acquisition; -1 = age unknown (card.time unavailable)
-notecard.sendRequest(req);
-// Total compact record: 4 floats × 4 + 5 int16s × 2 = 26 bytes per note
-```
-
-### Key code snippet 2: immediate-sync alert
-
-`sync:true` wakes the radio and flushes this Note immediately, bypassing the normal outbound window. The difference between a geofence breach alert reaching the operator in seconds versus hours can determine whether the equipment is recovered.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "geofence_breach");
-JAddNumberToObject(body, "lat", cur_lat);
-JAddNumberToObject(body, "lon", cur_lon);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 3: relay assertion on key-on edge
-
-The immobilizer does not cut power while the engine is running. It stages the relay and waits for the next OFF→ON ignition transition — *not* a level check. The previous wake's ignition state is persisted in `g_state.last_ignition_on` so the edge is detectable across sleep cycles. If the equipment was already running when the operator issued the immobilize command, the firmware deliberately holds off until the thief turns the key OFF and back ON; this prevents cutting the starter circuit while the engine is actively running and avoids the safety hazards called out in §5.
-
-```cpp
-bool ignition_edge = (!g_state.last_ignition_on && ignition_on);
-if (g_state.immobilize_pending && ignition_edge && !g_state.immobilized) {
- assertRelay();
- g_state.immobilized = true;
- g_state.immobilize_pending = false;
- sendAlert("ignition_on_immobilized", cur_lat, cur_lon, true, true);
-}
-```
-
-### Key code snippet 4: adaptive sleep via NotePayloadSaveAndSleep
-
-State is serialized before each sleep so the wake cycle has full context regardless of what triggered it. The duration adapts to the current risk level.
-
-```cpp
-uint32_t sleep_sec = afterhrs ? g_state.heartbeat_afterhours_s
- : moving ? g_state.heartbeat_moving_s
- : g_state.heartbeat_stopped_s;
-
-NotePayloadDesc save = {0, 0, 0};
-NotePayloadAddSegment(&save, kStateSegID, &g_state, sizeof(g_state));
-NotePayloadSaveAndSleep(&save, sleep_sec, NULL);
-```
-
-## 8. Data Flow
-
-
-
-**Collected every wake cycle.** Ignition state (12V voltage divider), motion status (Notecard accelerometer, read via `card.motion`), GPS fix (Notecard GNSS, read via `card.location`), battery voltage (`card.voltage`), UTC epoch (`card.time`). No I²C sensors external to the Notecard are used — all sensing is built into the Notecard for Skylo module.
-
-**Transmitted:**
-
-- `tracker.qo` — heartbeat Notes are **queued** at the moving/stopped sample rate (every 5 minutes while moving, every 60 minutes while stationary), then **transmitted** in batches on the `hub.set outbound` schedule (default 240 minutes, set by `outbound_min`). Moving heartbeats accumulate in the Notecard's local flash at the 5-minute sample rate but are not delivered to Notehub individually on that cadence — they are batch-flushed when the outbound window fires or when a `sync:true` alert happens to force an earlier session. Real-time pursuit tracking therefore relies on repeated alert syncs, not on the heartbeat delivery cadence. Under normal stationary operation a properly-secured job site generates **24 heartbeat Notes queued per 24 hours** (one per hour) and **6 outbound sessions per 24 hours** (one every 4 hours carrying ~4 heartbeats each), zero `alert.qo` Notes, and `fence_ok:1` in every heartbeat. Inbound sessions are context-aware: roughly 24 sessions/day while parked and daytime (every 60 minutes), and up to roughly 180 sessions during the 12-hour after-hours window (every 4 minutes) — all contributing to the daily radio-session count and energy budget.
-- `alert.qo` — emitted only on a rule trip, with `sync:true` to bypass the outbound queue. Each alert type (`geofence_breach`, `motion_after_hours`) has its own independent 5-minute cooldown so slow geofence oscillations or intermittent wind-buffeted vibration cannot flood the alert channel, and one alert type cannot suppress the other.
-
-**Routed.** Both Notefiles reach Notehub via cellular (LTE-M / NB-IoT) or, when cellular coverage fails, via Skylo satellite NTN (non-terrestrial network). Routing from Notehub to the operator's preferred downstream system is project-specific; see the [routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub).
-
-**Alert triggers:**
-
-- `geofence_breach` — cached GPS fix is more than `fence_radius_m` from the stored geofence center. The position used is the most recently cached fix from `card.location`; `fix_age_s` in the alert body indicates how stale that fix was at the moment the alert fired (`-1` if `card.time` was unavailable).
-- `motion_after_hours` — Notecard accelerometer reports `"mode":"moving"` during the after-hours window AND ignition is confirmed OFF. Ignition-off + motion is the characteristic signature of equipment being towed or loaded, not driven.
-- `ignition_on_immobilized` — an OFF→ON ignition edge is observed while `immobilize_pending` is set. Relay is asserted on the same wake cycle that detects the edge. This is a **staged, edge-triggered** immobilizer: the relay does *not* fire while the engine is already running, only on the next key-cycle the Cygnet observes (the previous wake's ignition state must have been OFF).
-- `immobilize_armed` — operator command received and staged; relay will fire on the next OFF→ON ignition edge that the Cygnet observes.
-- `cmd_retrieve_failed` — `note.get` on `immobilize.qi` exhausted all retries on this wake cycle; the command path is unhealthy. Rate-limited by `alert_cooldown_s` (default 5 minutes) so a persistent fault does not flood the operator inbox or satellite budget.
-
-**Inbound commands.** The operator sends `{"cmd":"immobilize"}` or `{"cmd":"release"}` to `immobilize.qi` from the Notehub UI or REST API. End-to-end command latency has two sequential components: first, the Notecard must complete an inbound sync session to pull the queued Note from Notehub (the inbound interval is context-aware: `inbound_min` default 4 minutes after-hours/moving, or `heartbeat_stopped_min` default 60 minutes when parked and daytime); second, the host Cygnet must wake and execute `setup()`, where `checkAndHandleCommand()` drains the entire `immobilize.qi` queue — calling `note.get` in a loop until the queue is empty, and applies each command in order (last command wins). The host wake interval is context-dependent: after hours (≤ 2 minutes), moving (≤ 5 minutes), stationary business hours (≤ 60 minutes by default). Combined worst-case end-to-end latency under default settings: **up to ~6 minutes** after-hours (≤ 4 minutes inbound sync + ≤ 2 minutes wake interval), **up to ~9 minutes** while moving (4 + 5 minutes), and **just under 2 hours** while stationary during business hours (up to 60 minutes daytime inbound sync + up to 60 minutes wake interval). The worst-case window — a stationary unit during business hours — is therefore just under **2 hours** from command post to `immobilize_armed` acknowledgment appearing in Notehub. If near-real-time command delivery is required around the clock, reduce `heartbeat_stopped_min` or see §11 Production next steps for the interrupt-driven ignition sense upgrade. Note that reliable inbound delivery requires cellular or WiFi connectivity — the Skylo satellite link is primarily uplink; commands staged while the unit is out of cellular coverage will be delivered when cellular is next restored.
-
-## 9. Validation and Testing
-
-**Expected steady-state cadence.** A correctly-deployed tracker on a secure job site generates `tracker.qo` heartbeat Notes at the stationary cadence (every 60 minutes by default, day and night), zero `alert.qo` Notes, and confirms `fence_ok:1` in every heartbeat. Notes accumulate in Notecard flash and are batch-transmitted in outbound sessions every 4 hours by default — expect 24 heartbeats queued per 24-hour period (6 outbound sessions, each carrying ~4 Notes). The device wakes every 2 minutes during after-hours to scan for motion and check inbound commands, but only emits a heartbeat when the 60-minute interval elapses. Any single `alert.qo` event warrants investigation.
-
-**Geofence first-light test.** With the unit powered and Notehub showing the device connected, set `fence_lat` and `fence_lon` in Notehub to the device's current coordinates (visible in Notehub's device view), and set `fence_radius_m` to a small value (e.g. `20`). Wait for the env-var update to reach the device (one inbound sync cycle, default 4 minutes).
-
-
-
-**Important:** Motion does **not** wake the host MCU. The host polls `card.motion` only when it wakes on its normal timer. During business hours, a stationary unit wakes every 60 minutes (default `heartbeat_stopped_min`) — physically moving the device will not trigger a response until the next scheduled wake. To make the test practical, do one of the following before moving the device:
-
-- Run the test during the after-hours window (default 6 PM–6 AM UTC), when the host wakes every 2 minutes.
-- Temporarily reduce `heartbeat_stopped_min` to `2` in Notehub env vars and wait for the update to be applied.
-- Connect USB serial to observe the active `setup()` cycle and move the device immediately after a wake.
-
-
-
-Once the device has woken with the updated geofence and the device is moved ~25 m outside the radius, a `geofence_breach` alert should appear in the Notehub event log **after the next qualifying wake cycle that has a sufficiently fresh GNSS fix**. Because geofence decisions use the most recently cached fix from `card.location` (bounded by the `card.location.mode` periodic interval, default 5 minutes), one or two wake cycles may pass before the Notecard acquires an updated fix that places the device outside the radius and triggers the alert. The alert fires within the same `setup()` that evaluates the breach — it is not deferred, but its timing is bounded by GNSS fix freshness, not by host-wake latency alone.
-
-**Immobilizer test.** From the Notehub UI, post `{"cmd":"immobilize"}` to `immobilize.qi`. Command delivery involves two steps that must both complete before the host acts: (1) the Notecard retrieves the Note on its next inbound sync; (2) the host Cygnet wakes and calls `note.get` in `checkAndHandleCommand()`. To make this testable in a reasonable time, either reduce `heartbeat_stopped_min` to `2` (matching the after-hours cadence) or bench-test during the after-hours window when the host wakes every 2 minutes. Watch the Notehub event log for `immobilize_armed` — that Note is queued by the host *after it processes the command*, so its appearance confirms the host wake cycle has completed. **The relay fires on an OFF→ON ignition edge, not a level check** — at least one wake must record ignition OFF before the firing wake records ignition ON. To simulate a key-on edge, ensure A2 is at 0 V (ignition divider output low) for one full wake cycle, then pull A2 to 3.3 V (or momentarily short the voltage divider output high) before the next wake. On the wake that observes the OFF→ON transition, `ignition_on_immobilized` should appear in `alert.qo` and the relay driver pin (A1) should go HIGH.
-
-**Power validation with Mojo.** Use the table below as a starting framework for sizing and diagnosing, not as a substitute for a bench soak. One important caveat applies to the published Notecard figures: Blues publishes Notecard current at **5 V** on the `VUSB` input — at the LiPo rail (~3.7–4.2 V) the same power dissipation appears as proportionally higher current. System-level totals (Cygnet + charger quiescent + relay driver) are not published by Blues; use the Mojo to measure them on your specific build.
-
-Published Notecard for Skylo figures are from the [NOTE-NBGLWX low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) and the [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/). System-level totals — Cygnet host, solar charger quiescent, and relay driver overhead added to the Notecard baseline — are not published by Blues and must be characterized on your specific build. Use the Mojo inline on the +VBAT rail to measure each phase rather than relying on generic estimates.
-
-| Phase | Notecard-only (published, @ 5 V) | Whole-device guidance (measure with Mojo) |
-|---|---|---|
-| Deep sleep (Cygnet off via ATTN, Notecard idle) | ~8–18 µA (published) | Notecard contributes ~8–18 µA; solar charger quiescent adds a few µA. Measure total on your build. |
-| Cygnet active, I²C transactions only (no radio) | — | Dominated by Cygnet active current. Measure with Mojo on your build. |
-| Notecard cellular session (LTE-M) | ~200–350 mA avg, ~650 mA peak (published) | Notecard dominates; Cygnet and relay driver add a small increment. Measure with Mojo for system totals. |
-| Notecard satellite session (Skylo NTN) | Not separately published | Measure with Mojo outdoors with confirmed satellite lock. Session current and duration depend on antenna placement and sky-view geometry — accurate characterization requires field testing. |
-
-**Sizing the energy budget.** Two independent cadences drive the radio, and both must be accounted for in the power budget:
-
-1. **Inbound sync sessions** — context-aware. While parked and daytime the Notecard inbound interval matches `heartbeat_stopped_min` (default 60 minutes), producing roughly **24 inbound sessions per day**. During the after-hours window the interval shortens to `inbound_min` (default 4 minutes), producing up to roughly **180 inbound sessions per 12-hour after-hours period**. In practice many of these are combined with outbound sessions by the Notecard, reducing the actual radio-on count. Under default settings the combined total is well under 210 sessions/day — significantly fewer than a constant 4-minute poll, and proportionally lower if the after-hours window is narrower.
-
-2. **Outbound sync sessions** — the Notecard transmits queued heartbeat Notes to Notehub on the `hub.set outbound` schedule (default 240 minutes, set by `outbound_min`), roughly **6 outbound sessions per 24 hours**. Each outbound session typically carries ~4 heartbeat Notes queued during the 4-hour window. Alert syncs (`sync:true`) add additional short bursts when a rule trips.
-
-The Cygnet host wake cycles (every 2 minutes after hours, every 5 minutes while moving, every 60 minutes at rest by default) are **host-only** active phases — a few seconds of I²C transactions with no radio involvement unless an alert fires during that wake.
-
-Use the Mojo bench soak to measure actual phase currents on your build, then estimate daily mAh draw for your deployment's specific cadence and size the LiPo and solar panel accordingly. Use the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) bench soak to validate your estimate: splice it inline on the LiPo-to-Notecarrier-CX +VBAT wire and measure a 24-hour soak. If you see continuous >50 mA draw, the Cygnet is not successfully entering deep sleep (usually a `card.attn` firmware or wiring issue). If you see the expected trace but alerts fail to transmit, check antenna placement and `hub.set` outbound configuration.
-
-Satellite session validation requires outdoor antenna placement with clear sky view toward the equator. The first satellite acquisition can take several minutes. See the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) for troubleshooting antenna placement and acquisition timing.
-
-## 10. Troubleshooting
-
-**Device not appearing in Notehub after power-on.**
-- Confirm USB power is stable (green LED on Notecarrier CX). If no LED, USB connection may be faulty.
-- Verify ProductUID is correctly pasted into the firmware sketch (top of `construction_equipment_anti_theft.ino`). Empty or mismatched ProductUID prevents auto-provisioning.
-- Check that antenna cables are fully seated in u.FL connectors. Loose MAIN antenna = no cellular lock, no Notehub connection.
-- If using Skylo satellite only (no cellular), first satellite acquisition can take 2–5 minutes with clear sky view. Orient antenna toward southern sky and wait 5 minutes before concluding failure.
-- Monitor Arduino Serial Monitor (9600 baud) if DEBUG_SERIAL is enabled in the firmware to see live I²C activity and Notecard responses.
-
-**Heartbeats appear but `fix_age_s` is always -1 or very large.**
-- `fix_age_s = -1` means `card.time` was unavailable when the Note was queued. This is normal on cold boot before the Notecard has synced with Notehub. After one successful outbound sync, `card.time` should populate and `fix_age_s` will show real elapsed seconds.
-- If `fix_age_s` remains `-1` after multiple outbound syncs, check that cellular or satellite sessions are completing (watch Notehub Events for successful sync activity). An isolated device never completes a session and never gets epoch time.
-- If `fix_age_s` is consistently >300 seconds (approaching `heartbeat_moving_min × 60`), the Notecard's GNSS module is not acquiring fresh fixes. Check antenna placement and sky view. GPS acquisition typically takes 20–60 seconds in open sky; metal roofs or urban canyons extend this significantly.
-
-**Geofence alerts never fire even though device moves outside the configured radius.**
-- Confirm that `fence_enabled` is set to `1` and `fence_lat`/`fence_lon` are populated in Notehub (Fleet Settings → Environment). Without `fence_enabled=1`, the firmware falls back to checking that both coordinates are non-zero, which fails at the prime meridian (0°,0°).
-- Verify that the device has woken since the env vars were set. Inbound sync interval is 4 minutes (default `inbound_min` during after-hours) or 60 minutes (default `heartbeat_stopped_min` during business hours). Watch the Events log for a heartbeat after you expect the sync to have completed.
-- If a heartbeat appears but still no alert after moving outside the radius, check the heartbeat's `fix_age_s`. If it is > 300 seconds (stale), the GNSS fix is old and may not reflect your current position. Move the device and wait for the next wake to allow GNSS to re-acquire — this can take 5 minutes if the device was stationary and GNSS was not active.
-- During business hours, the device wakes only every 60 minutes (default). To test geofence quickly, reduce `heartbeat_stopped_min` to `2` in Notehub, wait for the inbound sync to deliver it (~4 minutes), then move the device and wait for the next 2-minute wake cycle.
-
-**Immobilizer command appears in Notehub but relay never fires.**
-- Confirm that `immobilize_armed` appears in the Events log within the expected latency (worst-case: 60 minutes inbound sync + 60 minutes host wake = 2 hours during business hours). If `immobilize_armed` does not appear, the device never received the command.
-- If `immobilize_armed` appears, the command was staged but the relay did not fire. Relay fires on an OFF→ON ignition edge (not a level check). Ensure you simulated the edge correctly: A2 GPIO must be at GND (0V, ignition OFF) for at least one full wake cycle, then transition to 3.3V (ignition ON) before the next wake. The relay asserts only on the wake that observes the transition.
-- Check relay driver wiring: A1 GPIO → BSS138 gate. If A1 wiring is loose or shorted to GND, the relay cannot energize. Verify 10 kΩ pulldown is present (gate to GND) to keep MOSFET off when GPIO is high-impedance.
-- If relay coil shows 12V but does not click, check that 1N4007 diode is properly oriented (cathode to +12V, anode to MOSFET drain). Reversed diode will block the relay's energization path.
-
-**High power draw (Mojo shows >50 mA sustained).**
-- This indicates the Cygnet is not entering deep sleep. Check that `NotePayloadSaveAndSleep` is being called at the end of `runCycle()` and that `card.attn` mode is configured correctly.
-- Verify that DEBUG_SERIAL is commented out (not `#define DEBUG_SERIAL 1`). Serial debug slows startup and can prevent sleep entry.
-- If using bench USB, the Cygnet may not sleep reliably while powered from USB with Serial.begin() active. Disconnect USB and power from LiPo + solar charger to see true sleep behavior.
-- Check Notecard firmware version: `card.version` should show a recent Skylo-certified build. Outdated firmware may have high quiescent current. Update via Notehub [Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) if available.
-
-**Satellite acquisition fails or times out.**
-- Skylo satellites are in geostationary orbit (GEO) over the equator. Antenna must have a clear southern sky view. From northern hemisphere, satellite is low on the southern horizon — objects above 15° south-of-horizon elevation may block the link.
-- First satellite acquisition can take 2–5 minutes. Antenna orientation and sky clarity are critical. See [Satellite Best Practices](https://dev.blues.io/starnote/satellite-best-practices/) for detailed placement guidance.
-- If Skylo is the fallback and cellular is available, the device will prefer cellular and never trigger satellite. To test satellite, disconnect cellular or move the device to an area with no cellular coverage (e.g., rural location, underground bunker after the charger has been removed so it cannot fall back to USB).
-- Verify that Notecard for Skylo (NOTE-NBGLWX) antenna is the Skylo-certified antenna included with the kit. A substituted antenna may not lock onto the Skylo network. See datasheet for antenna part number.
-
-## 11. Limitations and Next Steps
-
-This POC demonstrates the control path, the data path, and the satellite fallback, but it deliberately stops short of being a production security device. A working immobilizer for a deployed fleet wants a latching relay that survives sleep gaps, a transient-protected ignition front end that can handle automotive load dump, and interrupt-driven wake that catches a key-on edge inside the next 100 ms rather than the next two minutes. Those are the items below, framed as the scope boundaries of the reference design and the engineering work that turns it into a production product.
-
-### Simplified for the POC
-
-The boundaries below are the scope choices of the reference design: each marks where the POC stops short of a production security device and points to the engineering work that closes the gap.
-
-**Staged, polled, edge-triggered immobilizer, not a continuously-held cut.** The immobilizer in this POC is a _next-wake / next-key-on-edge_ design. `NotePayloadSaveAndSleep` physically cuts the Cygnet's power during the sleep window, so the relay coil de-energizes on every sleep interval and the NC contact re-closes, briefly restoring the ignition circuit. The Cygnet re-asserts the relay only when it wakes and reads `immobilized = true`. For the default 2-minute after-hours cycle, the relay is active for only a few seconds every 2 minutes; a determined thief who turns the key during the sleep gap can crank the engine. Additionally, the relay only fires on an OFF→ON ignition edge: the previous wake must have observed ignition OFF and the current wake must observe ignition ON. If the equipment is already running when the immobilize command is staged, the firmware deliberately holds off until the next key cycle (this avoids cutting the starter circuit while the engine is actively running, per the safety guidance in §5). If the thief cycles the key fully between two Cygnet wakes (OFF→ON→OFF inside one sleep window), the firing edge is missed entirely until the next OFF→ON transition the Cygnet observes. This is a proof-of-concept staging mechanism. Production deployments require a **latching (bistable) relay** that holds its contact state with no continuous coil power and survives Cygnet sleep, power cuts, and wiring interruptions. See Production Next Steps below.
-
-**Ignition-sense front end is POC-only: no transient protection.** The bare 33 kΩ / 10 kΩ voltage divider provides no protection against automotive electrical transients. Load-dump events on generator sets can produce transients 40–200 V above nominal 12 V; starter-motor switching creates fast inductive spikes on the battery rail. Both can exceed the Cygnet GPIO absolute maximum and damage the MCU. A production design must add a TVS diode (e.g. 15 V unidirectional, placed before the high-side divider resistor) and an RC low-pass filter (e.g. 1 kΩ + 100 nF at the A2 node) as a minimum, or use a properly isolated sensing front end. The relay-side vehicle interface carries the same risk; the 1N4007 flyback diode protects the MOSFET drain from the relay coil's inductive spike, but does not protect the 12 V feed from upstream transients. The inline fuse in the BOM addresses overcurrent only.
-
-**No interrupt-driven ignition sense.** The firmware polls ignition state on each wake. A thief who turns the key and drives away within the 2-minute window between wakes will have a head start before the `ignition_on_immobilized` event fires. A hardware interrupt from the ATTN pin triggered by the ignition-sense line would wake the Cygnet instantly on key-on. The Notecarrier CX ATTN wiring and the `card.attn` `auxgpio` mode support this pattern, but it is not implemented in this POC.
-
-**Inbound commands require cellular.** The Skylo satellite link provides primarily uplink (device-to-cloud) data. `immobilize.qi` commands from Notehub to the device require a cellular or WiFi connection to be delivered. If the equipment is already in a dead zone, the operator must wait for cellular to be restored before the command arrives.
-
-**Geofence decisions use cached location data, and daytime detection latency is bounded by `heartbeat_stopped_min`.** The firmware calls `card.location` to read the Notecard's most recently cached GNSS fix; it does not wait for a fresh acquisition. The cached fix age is bounded by the GNSS acquisition cadence (`card.location.mode seconds`, default `heartbeat_moving_s` = 5 minutes), so one or two after-hours wake cycles may evaluate the geofence against the same stale fix before GNSS re-acquires. A geofence breach alert will not appear until both (a) the host wakes and (b) the Notecard has a sufficiently fresh fix that places the device outside the radius. During business hours, when the host wakes every 60 minutes (default), the worst-case detection latency for a theft that starts from a parked state is approximately 60 minutes plus up to 5 minutes of GNSS fix lag. Every outbound Note includes `fix_age_s` (seconds ≥ 0, or `-1` when `card.time` is unavailable) so operators can assess the staleness of each geofence decision.
-
-**Geofence is a circle.** The firmware uses a Haversine-computed circle around a single center point. Irregular job-site boundaries (L-shaped lots, equipment lots separated by roads) would require multiple overlapping circles or a polygon geofence, neither of which is implemented here.
-
-**Single geofence.** The device stores one home location. Equipment that legitimately moves between job sites (morning deliveries, weekend staging) will generate false geofence breach alerts unless the operator updates `fence_lat` / `fence_lon` in Notehub ahead of each move.
-
-**No tamper detection.** The firmware does not detect cable cutting, enclosure intrusion, or GPS jamming. A motivated thief who locates and removes the tracker, jams the antenna, or removes the equipment battery before the solar reserve is depleted will defeat this POC design.
-
-**Satellite data budget.** The Notecard for Skylo includes 10 KB of Skylo data per month. Compact record sizes derived from the actual template field types are **26 bytes** per `tracker.qo` heartbeat (4 floats × 4 bytes + 5 int16 × 2 bytes) and **47 bytes** per `alert.qo` event (4 floats × 4 bytes + 3 int16 × 2 bytes + 24-char string field at 25 bytes). The payload-only budget therefore supports roughly **390 heartbeat-sized Notes** (10,240 ÷ 26 bytes) or **215 alert-sized Notes** (10,240 ÷ 47 bytes) per month. Actual usable counts are lower after satellite-session protocol and framing overhead. Validate against Notehub usage metrics and field measurements rather than relying on payload-only arithmetic. Under normal stationary operation, heartbeats queue every hour and batch-transmit every 4 hours (~6 outbound sessions/day), so the heartbeat budget is well within limits. **Alert storm risk:** if the equipment is under active pursuit and cellular coverage is lost, repeated `sync:true` alerts every `alert_cooldown_min` (default 5 minutes) can consume the Skylo budget quickly: 47 bytes × 288 alerts/day (5-min cadence) = 13.5 KB, exceeding the monthly satellite allowance in a single day. If satellite-only operation during a pursuit event is expected, raise `alert_cooldown_min` to 15–30 minutes via the Notehub fleet env var to protect the monthly budget. In production, consider gating satellite transmission to alerts only and enforcing a per-Note minimum satellite interval.
-
-**Mojo is bench-only in this POC.** The firmware does not read Mojo's LTC2959 coulomb counter over the Qwiic bus. Mojo is used only as a bench measurement instrument during validation.
-
-### Production Next Steps
-
-The engineering work that turns this into a production security device follows, roughly from the most critical hardware change to fleet management.
-
-**A bistable (latching) relay** replaces the standard relay: a dual-coil type where one coil pulse sets the contact and a second pulse resets it, with no continuous coil current required to hold either state. Required characteristics are a dual-coil bistable mechanism, 12 V set and reset coils, a contact rating appropriate for the ignition/run-start control circuit, and an automotive-rated or equivalent environmental rating. Single-pulse `assertRelay` / `releaseRelay` from the Cygnet drives it, and the relay contact state survives Cygnet sleep, power interruption, and wiring cuts.
-
-**Interrupt-driven ignition sense** eliminates the polling gap: wire the ignition-sense signal to the Notecarrier CX's ATTN-compatible AUX GPIO input and configure `card.attn` `auxgpio` mode to wake the Cygnet instantly on ignition-on without waiting for the next timer expire.
-
-**Satellite data budgeting** protects the monthly allowance: track cumulative satellite Note count in state, suppress non-alert heartbeats from the satellite path, and enforce a minimum 15-minute satellite interval even during after-hours.
-
-**GPS jamming detection** catches an active countermeasure: if `card.location` returns `err` repeatedly while the Notecard accelerometer shows motion, log a `gps_jamming_suspected` alert.
-
-**Multi-geofence support** handles equipment that legitimately moves: store multiple geofence Notes in a `.db` Notefile — each Note is one site with lat/lon/radius — and have the firmware query all Notes and consider itself in-fence if it is within any of them.
-
-**A dedicated anti-tamper microswitch** on the enclosure lid connected to an AUX GPIO fires `enclosure_opened` as a Note if someone tries to physically remove the device.
-
-**[Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** enables wireless firmware updates to the Cygnet — essential for pushing updated geofence logic, new alert types, or threshold recipes to an entire fleet without any truck roll.
-
-## 12. Summary
-
-The fleet manager who used to learn about a stolen skid steer when the morning crew arrived now learns about it within minutes of the wheels moving, and has a staged immobilizer command ready to fire on the thief's next key-on. A Notecarrier CX and Notecard for Skylo, a solar-trickled LiPo, and two wires into the ignition circuit turn the equipment into a recoverable asset. Hourly heartbeats during the business day, rapid two-minute after-hours scans that fire immediate alerts on motion, and a staged relay that blocks ignition on the next key cycle the Cygnet observes. That's the loop, with a production latching relay as the clear next step. And when the equipment ends up exactly where stolen equipment ends up — rural stash yard, shipping container, metal barn — Skylo satellite carries the location and alert Notes skyward when cellular has long since gone dark. That's the difference between a recovered skid steer and one that's already been exported.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/construction-equipment-anti-theft-tracker-with-immobilizer/)**
diff --git a/63-construction-equipment-anti-theft-tracker-with-immobilizer/banner.png b/63-construction-equipment-anti-theft-tracker-with-immobilizer/banner.png
deleted file mode 100644
index 0aa80cbb..00000000
Binary files a/63-construction-equipment-anti-theft-tracker-with-immobilizer/banner.png and /dev/null differ
diff --git a/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/01-system-architecture.svg b/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/01-system-architecture.svg
deleted file mode 100644
index c2169401..00000000
--- a/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,90 +0,0 @@
-
diff --git a/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/02-wiring-assembly.svg b/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 9d8e32c6..00000000
--- a/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,123 +0,0 @@
-
diff --git a/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/03-data-flow.svg b/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/03-data-flow.svg
deleted file mode 100644
index 099b33e0..00000000
--- a/63-construction-equipment-anti-theft-tracker-with-immobilizer/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,100 +0,0 @@
-
diff --git a/65-off-grid-livestock-water-tank-monitor/README.md b/65-off-grid-livestock-water-tank-monitor/README.md
index fd0dac24..f0295274 100644
--- a/65-off-grid-livestock-water-tank-monitor/README.md
+++ b/65-off-grid-livestock-water-tank-monitor/README.md
@@ -1,500 +1,5 @@
# Off-Grid Livestock Water Tank Monitor
-
+A solar-powered cellular and satellite system for off-grid livestock water tanks that monitors water level, pump current, and battery voltage to alert ranchers before a stock tank runs dry.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Off-Grid%20Livestock%20Water%20Tank%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/livestock_water_tank_monitor/](firmware/livestock_water_tank_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a solar-powered [remote monitoring](https://blues.com/solutions-remote-monitoring/) system for off-grid livestock water tanks. It tells a rancher when a stock tank is going dry — without anyone driving the pasture roads to check. The device measures the water level, the pump's current draw, and the solar system's own battery voltage, then reports all three to a phone or dispatch system over cellular wherever a tower is reachable and over satellite via [Skylo](https://www.skylo.tech/)'s non-terrestrial network (NTN) where it isn't. The radio stays off between an immediate alert and a 4-hour summary, so the device runs indefinitely on a modest solar panel and a single battery.
-
-## What You'll Build
-
-After following this guide, you will have a solar-powered off-grid tank monitor that:
-- **Samples tank level, pump current, and battery voltage** every 15 minutes via analog sensors wired to a Notecarrier CX
-- **Alerts the rancher immediately** (via the [Blues Notehub](https://blues.com/notehub/) cloud service, with routes to SMS/push/webhook) when the tank drops below 20% full (alert) or 10% full (critical)
-- **Reports system health** (battery voltage, pump current) every 4 hours to a time-series database for trend analysis
-- **Falls back to satellite** (Skylo NTN) when cellular is unavailable, with the identical firmware handling both transports
-- **Runs for weeks on solar** even during cloudy stretches, thanks to an adaptive sleep strategy that extends the sample interval when the battery is low
-
-Expected data consumption: ~6 KB/month on the 500 MB included prepaid Blues data plan (cellular path; satellite is separate). First production event visible in Notehub within 4 hours of power-on.
-
-## 1. Project Overview
-
-**The problem.** Stock tanks in remote pastures are one of agriculture's oldest and most persistent operational problems. A rancher managing a spread across multiple pastures — each with its own poly or galvanized stock tank and a submersible pump pulling from a well — has to physically drive every road to verify that every tank is full. On a working ranch with pastures spread over thousands of acres, that inspection loop can take several hours and still miss a dry tank that empties between visits. The consequences aren't just inconvenience: cattle deprived of water for even a few hours in summer heat suffer rapid decline in health, and emergency water delivery is expensive even before factoring in animal losses.
-
-The failure modes are simple and repeatable. Tanks go dry because a float valve sticks or fails and the tank drains down, because the pump's supply well drops below the intake (the pump keeps running but moves no water), or because the pump motor fails entirely and nothing moves even when the float calls for it. None of these require sophisticated modeling — they are observable conditions that nobody happens to be watching. The sensor suite here measures exactly the two signals a rancher or hired hand would check on a physical inspection: how high is the water, and is the pump drawing current? The third measurement — solar battery voltage — tells you whether the monitoring system itself is healthy and likely to keep reporting through a run of cloudy days.
-
-**Why Notecard.** Stock tanks sit miles from the ranch house, beyond WiFi range, beyond LoRa range, and frequently beyond the reach of any terrestrial infrastructure. [Notecard for Skylo](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) addresses both halves of the connectivity problem in a single module: it uses cellular (LTE-M/NB-IoT) where a tower is reachable and falls back to the Skylo NTN satellite network where terrestrial coverage ends. Many ranch pastures sit in mixed coverage — a cellular signal may be available across most of a property, but valleys and remote corners go dark. A tank in a covered valley reports over cellular while a tank on a ridge beyond any tower still reports via satellite; the rancher sees both without understanding which network carried the data. No SIM activation, no carrier contract, no per-site configuration required. The Notecard also handles the low-power half: in periodic mode with a 15-minute sampling interval, the radio is active for tens of seconds every few hours and silent the rest of the time, making it possible to run this device indefinitely on a modest solar panel and a single battery even through overcast weeks in a northern winter.
-
-
-
-**Deployment scenario.** A weatherproof IP65 enclosure mounted on the tank post or fence rail adjacent to the tank opening. A MaxBotix ultrasonic level sensor peers down through the lid into the tank. A clamp-on current transformer clips around one conductor of the pump's supply lead without cutting any wire. A two-resistor voltage divider reads the 12V solar battery bus. A small solar panel, charge controller, and sealed battery live in or near the enclosure. Once installed and calibrated, the system reports tank levels and pump health to the rancher's phone through Notehub — requiring no physical site visit unless something goes wrong.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The onboard Cygnet STM32L433 host on the Notecarrier CX wakes on a 15-minute interval driven by [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn), reads three sensors (ultrasonic level, pump RMS current, solar battery voltage), evaluates alert thresholds, accumulates readings into a rolling window for the next summary, and returns to sleep. All Notecard interaction happens over I²C — no AT commands, no serial buffers, no JSON hand-rolling.
-
-**Notecard responsibilities.** Notecard for Skylo stores [Notes](https://dev.blues.io/api-reference/glossary/#note) in its on-device queue, selects the best available transport (cellular or Skylo satellite), manages the session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 4 hours), and flushes `sync:true` alert Notes immediately regardless of that cadence. Transport selection is transparent to the host firmware — the same `note.add` JSON that queues a Note over cellular also queues it over satellite. The Notecard also distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from Notehub on each inbound sync — alert thresholds, tank calibration values, and sampling intervals are all operator-tunable from the Notehub console without re-flashing the device.
-
-**Notehub responsibilities.** [Notehub](https://notehub.io) receives and stores every event — whether it arrived via the cellular or satellite transport, and applies project-level routes. Periodic summaries and alerts land in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile), so routes can fan them to different destinations: alerts to an SMS gateway or push-notification service, summaries to a long-term time-series store for trend analysis. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can classify devices automatically by pasture name or tank ID so that fleet-level environment variables encode site-specific calibration values without touching the firmware.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations; route configuration is project-specific and outside the scope of this reference design. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — this project ships no downstream endpoint.
-
-## 3. Technical Summary
-
-1. **Assemble hardware.** Order the BOM from §4, build the circuit per §5, and mount the enclosure on the tank post.
-2. **Flash firmware.** Clone this repo, set your Notehub ProductUID in the sketch, and flash via Arduino IDE. The FQBN below matches `firmware/livestock_water_tank_monitor/sketch.yaml`, so omitting `--fqbn` also works when invoked from the sketch directory:
- ```bash
- arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET \
- firmware/livestock_water_tank_monitor/livestock_water_tank_monitor.ino
- arduino-cli upload -p /dev/ttyACM0 --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET \
- firmware/livestock_water_tank_monitor
- ```
-3. **Set calibration.** Power on and wait for the device to sync to Notehub (first cellular connection within minutes if coverage exists, or satellite within ~10 minutes if cellular is unavailable). Once a `tank_status.qo` Note appears in Notehub, measure your actual tank geometry and set `tank_depth_mm` and `sensor_min_mm` as environment variables in Notehub (see §6 step 4 for the path). After the next sync, `level_pct` will report accurate percentages.
-4. **Set routes.** In Notehub, add two routes: one for `tank_alert.qo` (to SMS/push service) and one for `tank_status.qo` (to your time-series database or data lake). See §6 and the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub).
-
-Full assembly and calibration instructions follow in later sections; this quickstart gets you to first event in under an hour.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "tank_status.qo",
- "body": {
- "_time": 1717200000,
- "level_pct": 68.4,
- "distance_mm": 542.0,
- "pump_amps": 7.2,
- "pump_on": true,
- "battery_v": 12.8,
- "alerts": 0
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host — no separate MCU needed for this all-analog sensor mix. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)) | 1 | Cellular (LTE-M/NB-IoT/GPRS) where a tower is reachable; Skylo NTN satellite where it isn't. One module, one prepaid plan, no SIM activation or carrier contract — transport selection is fully automatic and transparent to the host firmware. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | **Bench-only commissioning tool** — spliced inline during validation to confirm the sleep/wake duty cycle. Not read by the deployed firmware; no power telemetry appears in transmitted Notes. Remove before field deployment. |
-| [MaxBotix HRXL-MaxSonar-WRL (MB7389)](https://maxbotix.com/products/mb7389) | 1 | IP67-rated weatherproof ultrasonic sensor; 300–5000 mm range, ±1 mm resolution, 2.7–5.5 V supply, and analog voltage output (V_cc/5120 per mm) require no microcontroller timing — a single ADC pin is all the interface needed. Internal temperature compensation keeps readings accurate across the wide ambient swings of an outdoor stock tank installation. |
-| [YHDC SCT-013-030 split-core CT, 30 A / 1 V voltage-output (e.g. SparkFun SEN-11005)](https://www.sparkfun.com/products/11005) | 1 | Clamp-on CT; measures pump RMS current without breaking or modifying the supply circuit. The 30 A range covers ½–2 HP submersible pumps typical of agricultural water systems. This design requires the **voltage-output** variant (30 A:1 V, built-in burden resistor); the current-output variant has no built-in burden and will damage the ADC pin without an external resistor. |
-| [SparkFun TRRS 3.5mm Jack Breakout (BOB-11570)](https://www.sparkfun.com/products/11570) | 1 | Exposes the CT's 3.5mm TRRS plug as screw terminals for wire termination. |
-| 10 kΩ 1% resistor (×2) | 2 | Bias divider for the CT circuit — forms a voltage midpoint at Vref/2 so the Cygnet ADC sees only positive voltages from the CT's AC output. |
-| 10 µF electrolytic capacitor | 1 | Bias-circuit decoupling; paralleled with the low-side bias resistor to stabilize the midpoint. |
-| BSS84 P-channel MOSFET, SOT-23 (or SI2301 or equivalent signal-level PMOS; widely available from DigiKey and Mouser) | 1 | High-side switch for the battery-voltage divider. Controlled via a 100 kΩ gate pullup and MMBT3904 level shifter so the divider is active only while the firmware samples A2. Prevents A2 back-powering through the MCU's input-protection diode whenever the host is unpowered during `card.attn` sleep. |
-| MMBT3904 NPN BJT, SOT-23 (or through-hole 2N3904 equivalent) | 1 | Level-shifts the 3.3 V Cygnet GPIO (A3) signal to drive the BSS84 gate, which is referenced to the 12 V supply rail. |
-| 47 kΩ resistor, 1% | 1 | High-side of the battery-voltage divider (from BSS84 drain to divider node). Scales the 12 V solar bus to the 3.3 V ADC range when the PMOS is on. |
-| 10 kΩ resistor, 1% (×2) | 2 | Low-side of the battery-voltage divider (×1) and base-drive resistor from Cygnet A3 to the MMBT3904 (×1). |
-| 100 kΩ resistor, ¼ W | 1 | BSS84 gate pullup to 12 V battery+. Ensures the PMOS switch is off by default when the host MCU is unpowered, keeping A2 at GND through the low-side 10 kΩ. |
-| 5V/1A step-down DC-DC regulator (e.g., [Pololu D24V10F5](https://www.pololu.com/product/2831)) | 1 | Derives stable 5V from the 12V solar battery bus; the 5.1–36V input range accommodates healthy and partial-charge battery states without dropout. |
-| 12V sealed lead-acid (SLA) or LiFePO₄ battery, ≥20 Ah | 1 | Field power reserve; LiFePO₄ preferred for wider operating temperature range and deeper discharge tolerance across cold-weather winters. Size to the expected run time between solar recharge events. |
-| [Renogy Wanderer 10A PWM Solar Charge Controller](https://www.renogy.com/products/wanderer-10a-pwm-charge-controller) | 1 | Manages panel-to-battery charging and protects against over-discharge via a low-voltage disconnect on the LOAD terminals. Any standard 12V PWM or MPPT controller with dedicated LOAD output terminals works; LOAD-output isolation is required for the power chain described in §5. |
-| [Renogy 20W 12V Monocrystalline Solar Panel](https://www.renogy.com/products/20-watt-12-volt-monocrystalline-solar-panel) | 1 | Sized for the energy budget of this design at most US latitudes; increase wattage for far-northern deployments or extended overcast seasons. |
-| Skylo-certified LTE-capable antenna — use the antenna included with the NOTE-NBGLWX kit (a Blues-qualified, dual-band antenna covering the LTE bands used for cellular plus the S-Band/L-Band frequencies on B23/B255/B256 used for Skylo NTN). The same antenna handles both cellular and satellite traffic; the NOTE-NBGLWX has a single `MAIN` u.FL antenna port for both transports. The Notecard is certified on Skylo's network **exclusively with the antenna provided in the kit** — replacing or modifying it results in an uncertified device that may be blocked by Skylo. If a different antenna is required, a Skylo delta-certification test is needed; see the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) and [contact Blues](https://blues.com/contact-sales/) for recommended test houses. | 1 | Must have a clear, unobstructed view of the equator-facing sky (southern sky in the northern hemisphere; northern sky in the southern hemisphere) for the satellite path. Mount flat on the enclosure lid or on an elevated bracket above the enclosure; never inside a metal enclosure. |
-| u.FL to SMA female pigtail, 100 mm+ (e.g. [SparkFun WRL-09145](https://www.sparkfun.com/products/9145) or equivalent RG316/RG178 assembly) + IP67-rated SMA bulkhead fitting | 1 each | Routes the included Skylo-certified antenna's connection from the NOTE-NBGLWX `MAIN` u.FL port through the enclosure wall. Match the bulkhead hole size to your enclosure; nickel-plated or stainless SMA bulkheads are widely available from DigiKey and Mouser. Use the shortest pigtail that reaches the bulkhead to minimize cable loss. |
-| IP65+ weatherproof enclosure, ≥8×6×3″ | 1 | Houses the electronics; mount on the tank post or adjacent fence rail with stainless hardware. |
-
-All Blues hardware ships with an active SIM including 500 MB of cellular data and 10 years of service — no activation fees, no monthly commitment. The NOTE-NBGLWX also includes Skylo satellite data as part of its service plan; see the [Blues pricing page](https://blues.com/pricing/) for current satellite data allotments and usage terms.
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. Notecard for Skylo seats into the M.2 slot. The Mojo is a bench power-measurement instrument spliced inline between the step-down regulator output and the Notecarrier's `+VBAT` pad during commissioning; it is not read by the deployed firmware and is removed before field installation.
-
-
-
-**Safety.** This installation involves the pump's supply conductors, a 12V battery, solar panel wiring, and charge-controller terminals — all of which can carry hazardous voltages or stored energy. **De-energize and lock out / tag out the pump circuit before installing or modifying any wiring.** Follow the pump manufacturer's and charge-controller manufacturer's installation instructions in full. Place a fuse in the positive battery lead as close to the battery terminal as practical (3–5 A for the monitor load; consult the charge-controller manual for the panel and battery leads). Electrical work should be performed by qualified personnel following applicable local electrical codes.
-
-
-
-**Solar power chain (panel, charge controller, battery):**
-- Solar panel **+** → charge controller **PV +** input; solar panel **−** → charge controller **PV −** input.
-- Charge controller **BATT +** → 12V battery positive terminal; charge controller **BATT −** → 12V battery negative terminal. If the controller does not include a fuse, place a 3–5A automotive blade fuse in the positive wire between the battery terminal and the controller's BATT + terminal.
-- Take the monitor load from the charge controller's **LOAD output terminals** (not directly from the battery terminals). This allows the controller's low-voltage disconnect to protect the battery from deep discharge during extended overcast periods.
-- **Common ground:** charge controller BATT −, the LOAD − output, and the 12V battery negative terminal must share a common ground bus. A short, low-resistance wire between them eliminates ground-offset measurement errors on the A2 battery-voltage divider.
-
-**Monitor power chain (charge controller LOAD → Notecarrier):**
-Charge controller **LOAD +** → Pololu D24V10F5 step-down input **VIN** (5V output) → Notecarrier CX `+VBAT`. The Notecarrier draws from `+VBAT` and internally generates the 3.3V rail for the host and sensors. During bench commissioning, splice the Mojo inline between the step-down output and `+VBAT` for power measurement, then remove it before field deployment. The battery-voltage divider top-side connects directly to the **battery terminals** (not the LOAD terminals) so it reads true battery voltage; see the voltage-divider circuit below for details on the PMOS high-side switch that controls when current flows through the resistor chain.
-
-**Level sensor (A0):**
-- MaxBotix MB7389 `V+` → Notecarrier `+3V3_OUT`
-- MaxBotix MB7389 `GND` → Notecarrier `GND`
-- MaxBotix MB7389 `AN` (analog voltage output pin) → Notecarrier `A0`
-- Leave the MB7389 `TX` (TTL serial) and `PW` (pulse-width) pins unconnected; only `AN` is used in this design.
-
-**Pump current sensor (A1):**
-Clamp the SCT-013-030 jaws around **one** conductor of the pump's supply cable (either leg, clamping both will cancel and read zero). Then wire the bias circuit:
-- Notecarrier `+3V3_OUT` → 10 kΩ resistor → **bias node** → 10 kΩ resistor → Notecarrier `GND`. These two resistors form a symmetric voltage divider; the bias node sits at approximately 1.65 V (Vcc/2).
-- SCT TRRS breakout `TIP` → bias node (the CT's AC output voltage rides on the DC bias midpoint)
-- SCT TRRS breakout `SLEEVE` → Notecarrier `GND`
-- Bias node → 10 µF electrolytic capacitor **positive** lead; capacitor **negative** lead → `GND` (the cap stabilizes the DC midpoint during pump-start transients)
-- Bias node → Notecarrier `A1`
-
-**Solar battery voltage divider (A2 + A3):**
-A BSS84 P-channel MOSFET acts as a high-side switch between the 12 V battery bus and the top of the voltage-divider resistor chain. Without this switch, A2 would remain driven by the battery bus whenever the host MCU is unpowered by `card.attn`, which can back-power the host through the A2 input-protection diode. A 100 kΩ gate pullup (battery+ → gate) keeps the PMOS off by default; an MMBT3904 NPN level-shifts the 3.3 V Cygnet GPIO (A3) to override that pullup and turn the PMOS on when a battery sample is needed.
-
-Wire the circuit as follows:
-- 12 V battery positive → BSS84 **source**
-- BSS84 **drain** → 47 kΩ resistor → divider node → 10 kΩ resistor → Notecarrier `GND`
-- Divider node → Notecarrier `A2`
-- BSS84 **gate** → 100 kΩ resistor → 12 V battery positive (gate pullup)
-- BSS84 **gate** → MMBT3904 **collector**
-- MMBT3904 **emitter** → Notecarrier `GND`
-- MMBT3904 **base** → 10 kΩ resistor → Notecarrier `A3`
-
-**Operation.** Firmware drives A3 HIGH before sampling: the MMBT3904 saturates, pulling the BSS84 gate to near GND (V_GS ≈ −12 V, well beyond the −0.8 to −2 V threshold). After the four ADC samples, firmware drives A3 LOW; the MMBT3904 turns off and the 100 kΩ pullup restores the gate to 12 V (V_GS = 0 V), turning the PMOS off within ≈ 100 µs. During the full `card.attn` sleep — even after MCU VDD is cut and A3 floats — the 100 kΩ pullup independently holds the PMOS gate at 12 V, leaving A2 at GND through the low-side 10 kΩ. No leakage path reaches the A2 protection structures.
-
-The voltage scaling is unchanged: `battery_V = V_adc / 0.175`, mapping 0–14.5 V to 0–2.54 V, within the 3.3 V ADC range. The divider connects to the battery terminals (not the LOAD terminals) so the MCU reads the true battery bus voltage.
-
-**Antennas:** The NOTE-NBGLWX exposes two u.FL ports — `MAIN` (cellular AND satellite, served by a single Skylo-certified LTE-capable antenna covering both LTE and S-Band/L-Band frequencies) and `GPS` (passive GPS/GNSS antenna; not used by this firmware and may be left unconnected). Connect a short u.FL-to-SMA female pigtail (≥100 mm, RG316/RG178) from the `MAIN` u.FL port to an SMA female bulkhead in the enclosure wall, then mate the included Skylo-certified antenna to the bulkhead from outside via its SMA connector. Use the antenna provided in the kit — substituting a third-party cellular antenna will invalidate the Skylo certification and may result in network blocking by Skylo. Route the antenna to the exterior of the enclosure lid or to an elevated bracket with an unobstructed view of the equator-facing sky (southern sky in the northern hemisphere; northern sky in the southern hemisphere). An antenna inside a metal enclosure or blocked by trees, structures, or terrain on the horizon toward the equator will fail to acquire Skylo GEO satellites — even when cellular reception is sufficient, deploy with sky view in mind so the satellite fallback path remains available. Refer to the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) for siting guidance.
-
-**Level sensor mounting:** Mount the MB7389 vertically (sensing face pointing straight down) through a 1.5–2 inch hole in a tank lid or on a bracket over the open top. Keep at least 300mm of clearance between the sensor face and the maximum expected water surface — this is the MB7389's blanking zone below which readings are unreliable. Record the actual mounting height above the full waterline and enter it as `sensor_min_mm` in Notehub after installation.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/livestock_water_tank_monitor/livestock_water_tank_monitor.ino` as `PRODUCT_UID`.
-2. **Claim the Notecard.** Power the unit; on first successful Notehub session, whether over cellular or Skylo satellite, the Notecard associates with your project automatically.
-3. **Create a Fleet per site.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration and routing. A natural unit here is one fleet per pasture — every tank in a pasture shares the same approximate tank geometry, pump type, and battery system. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can classify devices automatically by a tag or environment variable so you don't have to assign them manually as your fleet grows.
-4. **Set environment variables.** All variables are optional; firmware compile-time defaults apply if a variable is absent from Notehub. Changes sync to the device on the next inbound session — no re-flashing required. The two most important variables are the tank calibration values (`tank_depth_mm` and `sensor_min_mm`), which must be set per installation to get accurate level percentages.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `tank_depth_mm` | `1200` | Distance (mm) from the sensor face to the dry tank bottom. Measure with the tank empty. |
- | `sensor_min_mm` | `300` | Distance (mm) from the sensor face to the water surface when the tank is full. Must be ≥ 300 mm (the MB7389 blanking-zone minimum, below which the sensor cannot produce valid readings). The compile-time default equals the blanking-zone floor and is a placeholder — replace it with the measured full-tank distance during commissioning. |
- | `level_alert_pct` | `20` | Tank level (%) below which a `level_low` alert fires. The rancher has time to act. |
- | `level_critical_pct` | `10` | Tank level (%) below which a `level_critical` alert fires. Cattle may dewater within hours. |
- | `pump_on_amps` | `1.0` | RMS amps above which the pump is considered running. Set above the CT noise floor and below the pump's normal running current. |
- | `battery_alert_v` | `11.5` | Solar battery voltage (V) below which a `battery_low` alert fires. Indicates extended overcast weather or a charge controller fault. |
- | `sample_interval_sec` | `900` | Seconds between sensor samples. Default is 15 minutes. Reduce this during calibration (alongside `summary_interval_min`) so that each summary window contains at least one fresh sample; see §10. |
- | `summary_interval_min` | `240` | Minutes between periodic summary Notes. Default is 4 hours (6 Notes per day). **Must be at least ⌈`sample_interval_sec` ÷ 60⌉ minutes** — a summary window shorter than one sample wake contains no data. The firmware auto-clamps values below this floor at runtime; reduce `sample_interval_sec` proportionally when a short calibration window is needed (see §10). When this value changes, the firmware detects the difference on the next wake and re-issues `hub.set` with the updated cadence, so the Notecard's outbound sync interval stays aligned with the local summary schedule — no physical reboot required. |
- | `alert_cooldown_sec` | `3600` | Minimum seconds between repeated `level_low` and `level_critical` alerts. The `battery_low` alert is edge-triggered instead and is not subject to this cooldown — it fires once per episode and re-arms only after the voltage recovers above `battery_alert_v + 0.5V`. |
-
-5. **Configure routes.** Add at minimum one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `tank_alert.qo` (to an SMS gateway, push-notification service, or on-call webhook) and a second for `tank_status.qo` (to a time-series store for trend analysis and historical review). The Notefile separation means you can send alerts to your phone immediately while batching summaries to a database nightly, without any filtering logic in the route.
-
- **Example alert payload** (immediate, `sync:true`):
- ```json
- {
- "alert_code": 1,
- "level_pct": 8.5,
- "pump_amps": 0.0,
- "battery_v": 12.4,
- "_time": 1717200000
- }
- ```
- Alert codes: `0` = tank at 20% (low alert), `1` = tank at 10% (critical), `2` = battery below 11.5V.
-
- **Example status payload** (every 4 hours):
- ```json
- {
- "level_pct": 68.4,
- "distance_mm": 542.0,
- "pump_amps": 7.2,
- "pump_on": true,
- "battery_v": 12.8,
- "alerts": 3,
- "_time": 1717200000
- }
- ```
- `alerts` = count of `tank_alert.qo` Notes sent since the last summary; `pump_on` is derived from window-average `pump_amps` vs. the `pump_on_amps` threshold.
-
-## 7. Firmware Design
-
-The implementation spans three files in the [`firmware/livestock_water_tank_monitor/`](firmware/livestock_water_tank_monitor/) directory, with the `.ino` as the entry point:
-
-| File | Role |
-|---|---|
-| [`livestock_water_tank_monitor.ino`](firmware/livestock_water_tank_monitor/livestock_water_tank_monitor.ino) | Entry point. `setup()` orchestrates the full wake cycle: restore persisted state, load env-var cache, read sensors, evaluate alerts, emit the summary if due, and sleep via `card.attn`. `loop()` is intentionally empty. |
-| [`livestock_water_tank_monitor_helpers.h`](firmware/livestock_water_tank_monitor/livestock_water_tank_monitor_helpers.h) | Compile-time constants, the `GlobalState` struct, `extern` declarations for all cross-file globals, and helper function prototypes. |
-| [`livestock_water_tank_monitor_helpers.cpp`](firmware/livestock_water_tank_monitor/livestock_water_tank_monitor_helpers.cpp) | Sensor-read, env-var parsing, alert, and summary helper implementations. |
-
-**Dependencies:**
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) — the `note-arduino` library. Install via the Arduino Library Manager or download from the [note-arduino releases](https://github.com/blues/note-arduino/releases).
-
-### Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard configuration (`hub.set`, `card.motion.mode`) | `notecardConfigure` |
-| Re-issue `hub.set` when `summary_interval_min` env var changes | `reapplyHubSet` |
-| Template registration for the summary Notefile | `defineTemplates` |
-| Environment-variable fetch (each wake) | `fetchEnvOverrides` |
-| Ultrasonic raw distance reading | `readDistanceMm` |
-| Distance-to-level-percent conversion | `readLevelPct` |
-| Pump RMS current | `readPumpAmps` |
-| Solar battery voltage | `readBatteryV` |
-| Alert evaluation and emission | `evaluateAlerts`, `sendAlert` |
-| Periodic summary emission | `sendSummary` |
-| Sleep/wake state persistence | `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### Sensor reading strategy
-
-**Level sensor.** The MB7389 continuously outputs an analog voltage proportional to measured distance: V_out = V_cc / 5120 × distance_mm. Powered at 3.3V, the output maps to 0–3.22V across the 300–5000mm range, which sits cleanly within the Cygnet's 3.3V ADC reference. The firmware takes a 16-sample average of the 12-bit ADC counts to suppress noise, converts to millimeters, then maps to a 0–100% fill level using the calibrated `tank_depth_mm` (empty) and `sensor_min_mm` (full) values. Readings outside the MB7389's documented 300–5000mm range are flagged invalid and excluded from the accumulator. Both `distance_mm` and `level_pct` are accumulated across all valid samples in the summary window and emitted as window averages in the periodic summary Note.
-
-**Pump current.** The SCT-013-030 CT generates an AC voltage centered at the bias midpoint (~1.65V). The firmware first measures the DC bias from a 256-sample mean, then collects ~1500 samples (spanning approximately 20 AC mains cycles at the STM32's ADC throughput) and computes the root-mean-square deviation from that bias. The RMS voltage converts to current at 30A per volt. Readings below a 0.15A noise floor are clamped to zero to prevent ADC noise from generating false "pump running" detections.
-
-**Battery voltage.** A 4-sample average of the A2 ADC channel, scaled back through the voltage divider ratio: `battery_V = (adc × 3.3 / 4095) / (10 / 57)`. This measures the actual 12V solar battery bus voltage, not the regulated 5V rail — giving a genuine read on the system's energy state.
-
-### Event payload design
-
-Both Notefiles are [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates), storing Notes as fixed-length binary records instead of free-form JSON. Compact binary encoding reduces per-Note wire size by roughly 3–5× and, critically, is required for the NOTE-NBGLWX Skylo NTN satellite path — both templates are registered with `"format": "compact"` and an explicit port number so either Notefile can transit via satellite or cellular using the identical `note.add` call. Over a multi-year deployment generating 6 summary Notes per day, that compression also meaningfully extends the useful life of the 500 MB prepaid data budget. Alert Notes carry `sync:true` to request an immediate radio wake; fixed-length binary encoding means `pump_amps = 0.0` (pump off at alert time) is transmitted faithfully — there is no omitempty-style drop for zero-valued numeric fields in compact encoding.
-
-`tank_status.qo` (periodic, every `summary_interval_min`; compact template-encoded, port 50):
-
-```json
-{
- "file": "tank_status.qo",
- "body": {
- "_time": 1717200000,
- "level_pct": 68.4,
- "distance_mm": 542.0,
- "pump_amps": 7.2,
- "pump_on": true,
- "battery_v": 12.8,
- "alerts": 0
- }
-}
-```
-
-
-
-**`pump_on` is a derived field, not a per-sample boolean.** Each sample contributes its RMS current reading to a running average; at summary time `sendSummary` receives that 4-hour window average as `pumpAmps` and sets `pump_on = (pumpAmps >= pump_on_amps)`. A value of `true` means *the average pump current over the summary window was at or above the running threshold*, not that the pump ran continuously, and not that it ran for any particular fraction of the window. A pump that ran briefly at the start of a 4-hour window and then stopped can produce a non-zero `pump_amps` average with `pump_on: false`; a pump that ran continuously all window will produce `pump_on: true`. Use `pump_amps` (the numeric average) for trend analysis; treat `pump_on` as a coarse summary-window classification — `true` indicates the window-average current cleared the threshold, not that the pump ran continuously or for any particular fraction of the window.
-
-
-
-`tank_alert.qo` (immediate, `sync:true`; compact template-encoded, port 51):
-
-```json
-{
- "file": "tank_alert.qo",
- "body": {
- "_time": 1717200000,
- "alert_code": 0,
- "level_pct": 18.1,
- "pump_amps": 0.0,
- "battery_v": 12.6
- },
- "sync": true
-}
-```
-
-
-
-**`alert_code` is an integer type code:** `0` = `level_low`, `1` = `level_critical`, `2` = `battery_low`. The integer field keeps the compact-template payload fixed-length binary — the same requirement as the summary template for the NOTE-NBGLWX Skylo NTN path. All five fields (including `pump_amps = 0.0` when the pump is off) are always present in compact binary encoding.
-
-
-
-### Low-power strategy
-
-The Cygnet is awake for only a few seconds per 15-minute sample cycle. After each wake, `NotePayloadSaveAndSleep` serializes the runtime state (accumulator values, alert cooldown timestamps, summary window counter, and last-applied hub cadence) into Notecard flash, then triggers [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to assert the ATTN pin, which on the Notecarrier CX is routed to the board's `EN` input. Per the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/), `EN` "gates the board's host 3.3V rail," cutting the Cygnet host power entirely. The `+3V3_OUT` header pin is sourced from that same regulated rail, so sensors wired to it — the MB7389 level sensor and the CT bias network — are also unpowered for the full sleep interval. The battery-voltage measurement circuit is similarly off: the BSS84 PMOS gate is held at 12 V by the 100 kΩ pullup (independent of MCU power), leaving A2 at GND through the low-side 10 kΩ and drawing zero quiescent current from the battery bus. The NOTE-NBGLWX Notecard itself idles at approximately 18 µA @ 5V in its own [low-power idle state](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) between sessions, with the Skylo satellite modem also in low-power standby when not transmitting.
-
-Sampling and transmission are deliberately decoupled: the device samples every 15 minutes but transmits summaries only every 4 hours. Alerts bypass the transmit timer via `sync:true` — a critically low tank reaches the rancher's phone within a session-establishment window regardless of where the Notecard is in its outbound cycle. Over cellular, that window is typically 15–60 seconds. Over the Skylo NTN satellite link, session establishment takes longer — the Notecard must acquire the GEO satellite and complete the NTN registration before data flows, which can take a few minutes depending on satellite geometry and signal conditions. Alert Notes are still delivered via satellite when cellular is unavailable; the delivery latency is higher than over cellular, but the consequence of a missed alert on an off-grid tank with no other connectivity path is worse than a slightly delayed one. For sites with reliable cellular, the cellular path is used first and the satellite path is never exercised; satellite energy cost does not apply.
-
-When the firmware detects battery voltage below 12.0V (moderately discharged), it doubles the sleep interval to 30 minutes. Below `battery_alert_v` (default 11.5V, deeply discharged), it quadruples the interval to 60 minutes and fires a `battery_low` alert on the first crossing below that threshold. Further `battery_low` alerts are suppressed while the battery remains low and re-arm only after the voltage recovers above `battery_alert_v + 0.5V`. This edge-triggered approach prevents the 60-minute emergency-sleep cadence from issuing a `sync:true` radio session on every wake — which would worsen the very depletion it signals. The extended sleep intervals give the system a multi-day power reserve before the step-down regulator drops out, trading sampling granularity for survivability in extended overcast conditions.
-
-### Retry and error handling
-
-- The first Notecard request in `notecardConfigure` uses `notecard.sendRequestWithRetry(req, 10)` to absorb the cold-boot I²C race documented in the `note-arduino` library — the host can come up before the Notecard is ready to accept transactions.
-- The firmware tracks the last `outbound` value issued to `hub.set` in persisted state (`appliedSummaryIntervalMin`). After each `env.get`, if `summary_interval_min` differs from the persisted value, `reapplyHubSet()` re-issues `hub.set` using plain `sendRequest` (no retry, since the Notecard is already up on a wake-from-sleep path). This keeps cellular outbound cadence synchronized with the operator-configured summary interval without requiring a physical reboot.
-- Both `note.template` registrations (summary and alert) are persisted via a single `templatesInstalled` flag in the sleep payload. The flag is set only when both templates succeed; if either registration fails on cold boot (e.g. the Notecard is not yet ready), the flag stays false and `setup()` retries both registrations on every subsequent wake until both succeed. Without this, a single failed registration would leave one or both Notefiles in free-form mode, bypassing the compact binary encoding required for the satellite path.
-- Level readings outside the MB7389's valid range (300–5000mm) are excluded from the window accumulator. If an entire 4-hour window yields zero valid level readings, the summary emits `level_pct: -1.0` as a sentinel so downstream analytics can distinguish "sensor fault" from a real near-zero fill.
-- Pump current readings below the noise floor are clamped to 0.0 rather than excluded — a zero reading means "pump is off," not "no data."
-- `card.time` returning 0 (Notecard has not yet synced and obtained network time) causes the alert evaluator to skip threshold checks for that wake cycle, preventing spurious alerts on the first cold boot.
-- Alert de-duplication: `level_low` and `level_critical` alerts are cooldown-gated (one per `alert_cooldown_sec`, default 1 hour) so a slowly-draining tank doesn't page the rancher continuously. `battery_low` is edge-triggered — it fires once on the first crossing below `battery_alert_v` and is suppressed until the voltage recovers above `battery_alert_v + 0.5V`, preventing the 60-minute emergency-sleep cadence from issuing a `sync:true` radio wake on every cycle while the battery is already under stress.
-
-### Key code snippet 1 — template registration
-
-The template uses `"compact"` format and an explicit `port` — both required for the NOTE-NBGLWX Skylo NTN satellite path. Type hints are literal numeric values representing field types:
-- `14.1` = 32-bit float (e.g., `level_pct`, `pump_amps`, `battery_v`)
-- `12` = 2-byte signed integer / int16_t (e.g., `alerts` field, range −32,768 to +32,767)
-- `true` = boolean field (e.g., `pump_on`)
-- `14` = Unix timestamp field; auto-populated by the Notecard on each `note.add` — no change to firmware is needed
-
-Template-backed Notes reduce per-event wire size by ~4× and are required for satellite delivery.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", NOTEFILE_SUMMARY);
-JAddNumberToObject(req, "port", 50); // required for Skylo NTN
-JAddStringToObject(req, "format", "compact"); // required for Skylo NTN
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "_time", 14); // Unix timestamp (Notecard auto-fills)
-JAddNumberToObject(body, "level_pct", 14.1); // float32
-JAddNumberToObject(body, "distance_mm", 14.1); // float32
-JAddNumberToObject(body, "pump_amps", 14.1); // float32
-JAddBoolToObject(body, "pump_on", true); // boolean
-JAddNumberToObject(body, "battery_v", 14.1); // float32
-JAddNumberToObject(body, "alerts", 12); // int16 (2-byte signed, −32 768..+32 767)
-if (notecard.sendRequest(req)) {
- g.templatesInstalled = true; // retry on subsequent wakes if this fails
-}
-```
-
-### Key code snippet 2 — immediate alert with sync
-
-The alert Note uses the compact template registered for `tank_alert.qo` (port 51, `"compact"` format), so it travels over the NOTE-NBGLWX Skylo NTN satellite path when cellular is unavailable — the identical `note.add` call works over either transport. `sync:true` tells the Notecard to wake the radio immediately rather than waiting for the next scheduled outbound window. The `alert_code` integer (0 = `level_low`, 1 = `level_critical`, 2 = `battery_low`) keeps the body fixed-length binary as required by the compact template; all fields, including `pump_amps = 0.0`, are always present.
-
-```cpp
-if (!g.templatesInstalled) return false; // template required for satellite path
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", NOTEFILE_ALERT);
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "alert_code", alertCode); // 0=level_low, 1=level_critical, 2=battery_low
-JAddNumberToObject(body, "level_pct", levelPct);
-JAddNumberToObject(body, "pump_amps", pumpAmps);
-JAddNumberToObject(body, "battery_v", battV);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 3 — battery-adaptive sleep
-
-Sleep duration extends when the solar battery is low, conserving energy during multi-day overcast periods without requiring any cloud-side configuration change.
-
-```cpp
-uint32_t sleepSec = g_sampleIntervalSec; // default: 900 s (15 min)
-if (battV > 0.0f && battV < 12.0f) sleepSec *= 2; // 30 min: battery moderate
-if (battV > 0.0f && battV < g_batteryAlertV) // 60 min: battery critical
- sleepSec *= 2;
-
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, SEG_GLOBAL, &g, sizeof(g));
-NotePayloadSaveAndSleep(&payload, sleepSec, NULL);
-```
-
-### Key code snippet 4 — dynamic hub.set cadence
-
-When a Notehub operator changes `summary_interval_min`, the firmware detects the change on the next wake and re-issues `hub.set` so the Notecard's outbound cellular cadence stays synchronized — no physical reboot required.
-
-```cpp
-// After fetchEnvOverrides() returns true and updates g_summaryIntervalMin:
-if (envOk && g_summaryIntervalMin != g.appliedSummaryIntervalMin) {
- J *req = notecard.newRequest("hub.set");
- JAddStringToObject(req, "product", PRODUCT_UID);
- JAddStringToObject(req, "mode", "periodic");
- JAddNumberToObject(req, "outbound", (int)g_summaryIntervalMin);
- JAddNumberToObject(req, "inbound", (int)g_summaryIntervalMin);
- if (notecard.sendRequest(req)) {
- g.appliedSummaryIntervalMin = g_summaryIntervalMin;
- }
-}
-```
-
-## 8. Data Flow
-
-
-
-Every 15 minutes the Cygnet wakes, reads three sensors, and evaluates three independent alert conditions. Valid readings accumulate across wakes into a rolling window; each 4-hour summary averages each metric over that window.
-
-**Collected each sample.** Tank fill level (distance mm → level percentage), pump RMS amps, solar battery voltage (V), per-alert cooldown timestamps. (`pump_on` is not sampled directly, it is derived at summary time from the window-average of `pump_amps`; see §7.)
-
-**Transmitted.**
-- `tank_status.qo` — one compact template-encoded Note (port 50) every `summary_interval_min` (default 4 hours, 6 Notes per day). Every numeric field — `level_pct`, `distance_mm`, `pump_amps`, and `battery_v` — is the mean of valid samples collected over the window. A sensor that produced zero valid reads in the window emits `-1.0` as a sentinel; `alerts` carries the count of alert Notes sent since the last summary. `pump_on` is a derived boolean set at summary time: `true` when the window-average `pump_amps` is at or above `pump_on_amps` (default 1.0 A) — it reflects the window average, not a per-sample observation.
-- `tank_alert.qo` — compact template-encoded Note (port 51) emitted immediately on any threshold trip with `sync:true`. The compact template and explicit port are required for the Skylo NTN satellite path; the identical `note.add` delivers via cellular or satellite depending on which transport the Notecard selects. `alert_code` is an integer field (0 = `level_low`, 1 = `level_critical`, 2 = `battery_low`). `level_low` and `level_critical` are cooldown-gated (one per `alert_cooldown_sec`, default 1 hour) so a slowly-drifting tank doesn't generate hundreds of pages. `battery_low` is edge-triggered: one alert fires on the first crossing below `battery_alert_v` and further alerts are suppressed until the voltage recovers above `battery_alert_v + 0.5V`.
-
-**Alert triggers.**
-- `level_low` — tank fill percentage below `level_alert_pct` (default 20%). The rancher has time to act; typical time-to-empty at normal cattle consumption is several hours at this level.
-- `level_critical` — tank fill percentage below `level_critical_pct` (default 10%). Urgent: cattle dehydration risk is hours away, not days.
-- `battery_low` — solar battery voltage below `battery_alert_v` (default 11.5V). Indicates extended cloudy weather, a faulty solar panel, or a charge controller problem — the monitoring system itself is at risk of going offline.
-
-**Routed.** Both Notefiles flow to Notehub and from there to whatever downstream the project's routes specify. The deliberate Notefile separation means `tank_alert.qo` can go to an SMS gateway for immediate rancher notification while `tank_status.qo` goes to a time-series database for historical trend analysis and predictive maintenance — without any per-Note filtering logic in the routes.
-
-## 9. Validation and Testing
-
-**Expected cadence in normal operation.** A healthy, full tank generates one `tank_status.qo` Note every 4 hours and zero `tank_alert.qo` Notes. After first power-on, confirm Notehub shows a summary Note within 4 hours and that `level_pct` is non-zero and plausible given the tank's actual fill state.
-
-**Tank calibration.** `distance_mm` in `tank_status.qo` is a 4-hour window average, not a point-in-time measurement. Reading it immediately after filling or draining the tank will give a value averaged over a window that spans multiple water levels and will not match the current surface distance. Before calibrating, set **both** `sample_interval_sec` to `60` (1 minute) and `summary_interval_min` to `5` in Notehub, then wait for the device to pick up both changes on its next inbound sync. Summary cadence is bounded by the sample wake cadence — the firmware enforces `summary_interval_min ≥ ⌈sample_interval_sec ÷ 60⌉`, so reducing `summary_interval_min` alone without also reducing `sample_interval_sec` leaves the effective window length unchanged. With both set as above, the device wakes every minute and emits a summary every 5 minutes; once the tank has been held at a stable level for one full 5-minute window, the averaged `distance_mm` closely approximates the actual surface distance.
-
-Fill the tank completely and keep it at its overflow point for at least one full 5-minute window. Note the `distance_mm` value in the summary Note that follows — that value is `sensor_min_mm`. With the tank **completely drained to the bare bottom** — no residual water — hold it at empty for another full window and Note `distance_mm` again; that value is `tank_depth_mm`. A calibration performed with the tank "nearly empty" rather than truly empty will underestimate `tank_depth_mm`, compressing the level-percentage range and causing all readings to read high. If you cannot safely drain the tank, record the actual residual depth in millimeters and add it to the measured `distance_mm` before entering it as `tank_depth_mm`. Enter both calibrated values as device-level environment variables in Notehub and reset both `sample_interval_sec` and `summary_interval_min` to their operational values (defaults `900` and `240` respectively). After the next inbound sync, `level_pct` should read ~100% when full and ~0% when empty.
-
-**Pump current verification.** `pump_amps` in `tank_status.qo` is a 4-hour window average. Comparing a handheld clamp-meter reading against a summary Note produced during normal operation will give a misleading result if the pump ran intermittently during that window. To get a comparable number, first set both `sample_interval_sec` to `60` and `summary_interval_min` to `5` in Notehub (as for tank calibration above) and wait for the device to pick up the changes. Once the pump has been running continuously at a steady load for one full 5-minute window, clamp a calibrated AC clamp meter around the same conductor the CT monitors and Note the reading; it should agree with `pump_amps` in the next summary Note within a few percent. A discrepancy of more than a few percent typically indicates the CT jaws are not fully closed, or the CT is clamped around both conductors (cancellation). A reading near zero with the pump running suggests a wiring fault in the bias circuit midpoint. Reset both `sample_interval_sec` and `summary_interval_min` to their operational values when verification is complete.
-
-**Simulating an alert.** Lower `level_alert_pct` to 99 in Notehub; the next inbound sync will pull the new value and the following sample cycle will fire a `level_low` alert visible in Notehub within the cellular session-establishment window. Reset to the desired production threshold when confirmed.
-
-**Using Mojo to validate power behavior (bench-only).** The [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) is bench instrumentation in this design — the deployed firmware does not read the Mojo's LTC2959 coulomb counter at runtime and no power telemetry appears in transmitted Notes. Use it during commissioning to verify the sleep/wake profile before field installation.
-
-Splice the Mojo inline between the step-down regulator output and the Notecarrier's `+VBAT` pad. The NOTE-NBGLWX Notecard datasheet specifies approximately 18 µA @ 5V in the Notecard's own low-power idle state — that is the bare Notecard chip's quiescent draw at its VMODEM_P supply, measured in isolation. A Mojo spliced at `+VBAT` measures the full load on that rail: the Notecard plus the Notecarrier CX's own board quiescent consumption (regulators and any always-on carrier-board circuitry). Expect the measured idle floor to sit somewhat above the bare-Notecard datasheet figure. The Cygnet host, cut by `card.attn`, draws essentially zero from the 5V rail; the battery-voltage PMOS switch holds A2 at GND — those paths contribute nothing during sleep. Rather than targeting the bare-Notecard datasheet number, use the Mojo trace to validate the *shape* of the power profile: long flat idle intervals at the system quiescent floor, interrupted by brief active pulses every 15 minutes (Cygnet on + sensor reads + Notecard I²C transaction), with one longer radio burst per outbound sync.
-
-*Cellular session profile:* Cellular transmit sessions run approximately 250 mA average at the 5V rail for 10–60 seconds per outbound sync. Over a 4-hour summary cycle (16 sample wakes), the expected trace is 16 short pulses of several mA (Cygnet active plus ADC reads, ~3–5 seconds each), separated by 15-minute stretches at ~18 µA, with one ~250 mA burst when the cellular session opens. Total energy for the 4-hour window should be well under 5 mAh from the 5V rail.
-
-*Satellite session profile:* When the Notecard operates on the Skylo NTN path, the power picture changes substantially. The NOTE-NBGLWX idles at the same ~18 µA @ 5V as in cellular mode, but the modem-active phase for a satellite sync spans several minutes rather than the 10–60 seconds typical of a cellular session — session duration, not per-moment current draw, is what makes per-session satellite energy meaningfully higher. The [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) (Power Information section) documents modem-active draw at approximately 250 mA @ 5V; no separate NTN-specific sub-phase current is published because the BG95-S5 NTN modem-active draw falls within the same envelope as cellular. The VMODEM_P supply must sustain 750 mA to cover peak modem activity in either mode:
-
-| Phase | Current draw | Typical duration |
-|---|---|---|
-| Idle (between sessions, cellular and satellite radios in standby) | ~18 µA @ 5V | continuous between syncs |
-| Cellular session (LTE-M/NB-IoT acquisition + data transfer) | ~250 mA average @ 5V | 10–60 seconds per outbound sync |
-| Satellite acquisition + NTN registration (GEO search + Skylo attach) | ~250 mA average @ 5V — the BG95-S5 NTN modem-active draw is in the same range as cellular; sub-phase variation across search, attach, and idle-wait between attempts is not broken out in the NOTE-NBGLWX datasheet | 2–5 minutes nominal; up to 10 minutes in marginal sky-view or poor satellite-geometry conditions |
-| Satellite data transfer (NTN payload upload to Skylo) | ~250 mA average @ 5V | tens of seconds, following successful acquisition |
-
-Per-session energy from the published 250 mA figure: a 30-second cellular sync draws approximately **2 mAh** at the 5V rail; a 3-minute satellite session (acquisition + data transfer combined) draws approximately **12.5 mAh** — roughly 6× more per sync. At the default 4-hour summary interval, six cellular syncs per day total approximately 12 mAh in radio energy; six satellite syncs total approximately 75 mAh per day. For sites where satellite is the primary or frequent transport, size the solar panel and battery for the ~75 mAh/day satellite figure, not the ~12 mAh/day cellular figure.
-
-On a satellite-primary Mojo trace you will see: an ~18 µA floor between syncs, then a multi-minute plateau at approximately 250 mA average (satellite acquisition + Skylo NTN registration + data transfer) before the radio returns to idle — qualitatively different from the short cellular burst. Both the acquisition and data-transfer sub-phases appear as a single extended block at modem-active current levels on a Mojo power trace; if acquisition stalls due to poor sky view or satellite geometry, the plateau extends beyond the 2–5 minute nominal without any step-change in current that would be visible on the trace. If the site has reliable cellular, the satellite path is never exercised and the cellular profile applies.
-
-If the Mojo trace shows continuous multi-mA draw with no idle transitions, the host MCU is not sleeping — check `card.attn` wiring and the `NotePayloadSaveAndSleep` call.
-
-**Validating the satellite path.** To confirm satellite delivery before field deployment, take the device to a location with no usable cellular coverage and trigger a `tank_alert.qo` Note by lowering `level_alert_pct` to 99 in Notehub. With a clear equator-facing sky view, the NOTE-NBGLWX will switch to the Skylo satellite path. Confirm the alert Note appears in Notehub — it will carry the same body as a cellular-delivered Note but the session metadata will reflect the satellite transport. Session establishment may take several minutes; allow up to 10 minutes before concluding satellite is not working. Common issues: antenna blocked or not facing the equator-facing direction, obstructions between the antenna and the horizon, or the device too close to a building or tree line. Refer to the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/) for a complete troubleshooting checklist.
-
-## 10. Troubleshooting
-
-| Symptom | Root Cause | Fix |
-|---------|-----------|-----|
-| No `tank_status.qo` Notes appear in Notehub after 4 hours | Device has not synced yet; check connectivity | If indoors or in a valley, move the antenna to an exterior location with sky view. For cellular, confirm you are in LTE-M/NB-IoT coverage (check a carrier map). For satellite, confirm the antenna has unobstructed equator-facing sky view. |
-| `level_pct` reads 0% or 100% regardless of actual tank fill | `tank_depth_mm` or `sensor_min_mm` not calibrated correctly | Follow the calibration procedure in §10. Tank must be held at full and completely empty for one 5-minute window each while sampling every 60 seconds. |
-| `level_pct` fluctuates wildly or reads negative | Sensor is outside the 300–5000 mm range, or mounting is unstable | Check that the MB7389 is mounted vertically (face pointing straight down) with at least 300 mm clearance above the maximum water surface. Secure the mounting bracket so it does not vibrate. |
-| `pump_amps` reads 0 even when pump is running | CT is not clamped around the pump supply line, or CT jaws are open | Verify the CT jaws are fully closed around a single conductor of the pump supply cable. Check the bias circuit wiring (10 kΩ divider, 10 µF cap, Notecarrier A1). Use a calibrated clamp meter on the same conductor to verify current is present. |
-| Device sends an alert every 15 minutes instead of respecting `alert_cooldown_sec` | Alert cooldown timer was not persisted across a reboot (state loss from power interruption) | This is expected behavior immediately after power-on if the Notecard's real-time clock (RTC) has not yet synced to the network. Once the first network sync completes, RTC is set and cooldown works. If alerts continue to repeat after multiple syncs, check `alert_cooldown_sec` in Notehub and confirm it is present in `env.get` responses (enable `TANK_MONITOR_DEBUG` in the sketch to view I²C traces). |
-| Device stops reporting after several days | Battery voltage has dropped below the critical-discharge threshold | Check the solar panel is unobstructed and receiving adequate sunlight. Check the charge controller's LOAD output is supplying 5V to the step-down regulator. If the panel or controller is faulty, the battery will deplete and the device will go silent when the step-down regulator voltage drops below ~4.3V. Check Notehub's last-seen timestamp to estimate when the device lost power. |
-| Satellite acquisition times out (no Note appears in 10+ minutes) | Antenna has poor or no sky view, or device is too close to structures that block the horizon | Move the antenna outdoors or to an elevated position with unobstructed equator-facing sky (south in northern hemisphere, north in southern hemisphere). Refer to the [Satellite Best Practices guide](https://dev.blues.io/starnote/satellite-best-practices/). If the site truly has no equator-facing sky view, satellite will not work; confirm cellular coverage as fallback. |
-
-## 11. Limitations and Next Steps
-
-This reference design is a single-tank monitor that works on a pole next to a stock tank — not a multi-tank irrigation controller, not a herd-level analytics platform, not a polar-region satellite design. The list below names the boundaries that come with that scope so a rancher or integrator can see exactly where the design ends and where production work begins.
-
-### Simplified for this reference design
-
-Each of the simplifications below is a deliberate scope choice — a place where a production rancher or integrator will want to add a sensor, a calibration step, or an integration once the basic single-tank monitor is proven in the field.
-
-**Satellite antenna siting is site-specific.** The NOTE-NBGLWX Skylo path requires a clear, unobstructed view of the equator-facing sky (southern sky in the northern hemisphere; northern sky in the southern hemisphere). Tank sites in valleys, near tree lines, or adjacent to structures that block the horizon toward the equator may have degraded satellite performance or **no satellite link at all**. Survey the site for equator-facing sky exposure before selecting this design for a satellite-dependent deployment. For sites that require Iridium coverage (polar regions, sites where Skylo GEO coverage does not reach), a different satellite module is needed.
-
-**Satellite data budget** deserves attention on any deployment that is frequently on the satellite path: monitor satellite data consumption in the Notehub usage dashboard, and consult the [Blues pricing page](https://blues.com/pricing/) for current satellite data allotments and applicable usage terms.
-
-**Manual calibration** is required at commissioning. `tank_depth_mm` and `sensor_min_mm` must be measured and entered in Notehub per installation. A production design would include a commissioning sequence that auto-measures and stores these values during the first fill cycle.
-
-**Single-phase current sensing** is all the CT provides, since it clamps around one conductor. Three-phase deep-well pump motors (common in high-volume agricultural wells) need three CTs and a firmware change to sum the three RMS values.
-
-**No pump fault-to-start detection.** The firmware detects when the pump *is* running (current above threshold) but cannot determine whether the pump *should* be running. Adding a float-switch digital input on a Cygnet GPIO would enable a "pump failed to start" alert pattern — float says the tank needs water but no pump current is detected.
-
-**No local sample history.** The Notecard queues Notes for multi-day transmission if both cellular and satellite are unavailable, but the firmware keeps only the current summary window in RAM. A device reset (lightning strike, power interruption) clears the in-progress accumulator and loses the partial window's data.
-
-**Mojo is bench-validation equipment.** The firmware does not read the Mojo's LTC2959 coulomb counter over I²C at runtime; `alerts` is the only meta-field in the summary. Adding a `mojo_mah` field for fleet-level power telemetry is a straightforward extension.
-
-### Production Next Steps
-
-Once a rancher is running the basic tank monitor, the following extensions are the natural progression — from the most immediately useful detection improvements toward the deeper fleet-management integrations.
-
-**Float-switch input on a digital GPIO** adds "pump should be running but isn't" detection, closing the gap left by current-only sensing.
-
-**Over-the-air firmware updates** via [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) are critical for a device that lives in a pasture for years between physical access.
-
-**Three-phase CT support** brings high-volume wells into scope: A1, A4, A5 for three CTs (A3 is occupied by the battery-divider PMOS enable), with firmware summing three RMS values into a single `pump_amps` field.
-
-**A per-device commissioning wizard** — a "first-boot setup mode" — would guide the installer through measuring and storing calibration values before entering normal operation.
-
-**`env.get` with the `time` argument** fetches environment variables only when they have changed since the last sync, reducing I²C overhead on each wake.
-
-**Satellite data usage monitoring** — a Notehub route or environment variable feedback loop — would alert operators when satellite data consumption approaches the plan's included allotment.
-
-## 12. Summary
-
-A Notecarrier CX and Notecard for Skylo — paired with a weatherproof ultrasonic level sensor, a clamp-on current transformer, and a handful of passive components — give a rancher continuous visibility into every stock tank on the property without driving the pasture roads. Notecard for Skylo uses cellular where a tower is reachable and falls back to the Skylo satellite network where terrestrial coverage ends, so tanks in covered valleys and tanks on remote ridges both report through the same firmware and the same Notehub project. Sampling happens locally every 15 minutes with the host MCU asleep between measurements; the radio wakes only for the 4-hour summary or an immediate alert. That duty cycle makes the system comfortable running indefinitely on a modest solar setup, with built-in power-aware sleep extension to handle extended overcast stretches in northern winters. Dry-tank alerts and solar fault alerts reach the rancher's phone before cattle suffer from dehydration; pump current is captured as observational telemetry in every summary and alert payload so operators can correlate it with tank levels after the fact. It's a simple problem — knowing when the tank is low — and the Blues stack is a proportionately simple solution: a sensor, a Notecard, and a network path that follows the asset wherever it lives.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/off-grid-livestock-water-tank-monitor/)**
diff --git a/65-off-grid-livestock-water-tank-monitor/banner.png b/65-off-grid-livestock-water-tank-monitor/banner.png
deleted file mode 100644
index 0635925f..00000000
Binary files a/65-off-grid-livestock-water-tank-monitor/banner.png and /dev/null differ
diff --git a/65-off-grid-livestock-water-tank-monitor/diagrams/01-system-architecture.svg b/65-off-grid-livestock-water-tank-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index a63bee81..00000000
--- a/65-off-grid-livestock-water-tank-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/65-off-grid-livestock-water-tank-monitor/diagrams/02-wiring-assembly.svg b/65-off-grid-livestock-water-tank-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 24cd7fd1..00000000
--- a/65-off-grid-livestock-water-tank-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,103 +0,0 @@
-
diff --git a/65-off-grid-livestock-water-tank-monitor/diagrams/03-data-flow.svg b/65-off-grid-livestock-water-tank-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index 0705f7b6..00000000
--- a/65-off-grid-livestock-water-tank-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/66-remote-apiary-hive-health-monitor/README.md b/66-remote-apiary-hive-health-monitor/README.md
index a2b76367..fe242e62 100644
--- a/66-remote-apiary-hive-health-monitor/README.md
+++ b/66-remote-apiary-hive-health-monitor/README.md
@@ -1,561 +1,5 @@
# Remote Apiary Hive Health Monitor
-
+A solar-powered cellular hive monitor that tracks hive weight and brood-box climate every 15 minutes and captures a daily acoustic snapshot of the colony, without grid power.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Remote%20Apiary%20Hive%20Health%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/apiary_hive_monitor/](firmware/apiary_hive_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a solar-powered hive monitor that turns each hive into a [remotely-monitored asset](https://blues.com/solutions-remote-monitoring/) — tracking hive weight and brood-box climate every 15 minutes, capturing a brief acoustic snapshot of the colony once per day, and reporting it all without grid power and without a truck roll every time the data looks wrong. A single [Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) carries cellular, WiFi, and Skylo satellite radios on one module and fails over between them automatically, so the same hardware and firmware reach the [Blues Notehub](https://blues.com/notehub/) cloud service from an orchard with cell coverage or a meadow beyond any tower.
-
-## 1. Project Overview
-
-
-**The problem.** Commercial beekeepers run apiary yards in orchards, fields, and forest edges where there is no electrical infrastructure and no cellular signal worth depending on. A full-sized Langstroth hive in production can weigh 40–80 kg, and the signs that separate a thriving colony from a dead one — or from a hive that has just swarmed and taken 10,000 bees with it — are measured in kilograms, degrees, and the subtle shift in pitch of the colony's acoustic hum. None of those signals are observable from a distance without instrumentation, and traditional hive inspection — opening the box, smoking the bees, physically weighing the stand — takes time, disturbs the colony, and simply can't happen every 15 minutes. The result is that hive collapse, swarm departure, and queen loss are routinely discovered days after the fact, when the economic damage is already done.
-
-This project is the remote set of eyes and ears that a beekeeper wants but cannot afford to physically station at every yard. A single weatherproof enclosure mounted on the hive stand reads three independent signals: weight (colony biomass and honey stores), temperature and humidity inside the brood box (brood viability), and acoustic features sampled from an analog microphone inside the hive (behavioral state). Those three signals are processed locally — weight and temperature sampled every 15 minutes, audio captured once per day — then aggregated into a daily summary and pushed to Notehub: over cellular where a tower is in reach, and over the [Skylo](https://www.skylo.tech/) satellite network where one isn't. Notecard for Skylo selects the path automatically. The beekeeper gets one notification per day in steady state and an immediate alert when any signal crosses a threshold.
-
-
-
-**POC scope — daily audio snippet, features only.** The acoustic path captures a single ~0.75-second snippet once per summary window (default: once per day). It computes zero-crossing rate (ZCR), RMS energy, and peak amplitude entirely on-device and transmits only those three numbers per summary. No raw audio is buffered, stored, or transmitted. Capturing audio once daily rather than every 15 minutes is consistent with the project scope — colony acoustic state evolves on the scale of hours, not minutes, and avoids the power and payload cost of continuous acoustic sampling. See §7 and §10 for details.
-
-
-
-**Why Notecard for Skylo.** Apiaries are deliberately sited away from human infrastructure — the whole point is to place bees near crops, meadows, or forest edges where forage is available and human disturbance is minimal. Those sites have no AC power and often marginal or absent cellular coverage. Every one of those constraints rules out the conventional IoT stack: a gateway-plus-cloud-SIM architecture requires months of site negotiation per yard, and a device that only works in strong cellular signal will fail at exactly the locations where bees are most productive. [Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) (NOTE-NBGLWX) carries three radios on one M.2 module — cellular (LTE-M / NB-IoT / GPRS) with its own prepaid global SIM, WiFi, and satellite over the [Skylo](https://www.skylo.tech/) non-terrestrial network (**NTN**) — and selects among them automatically. There is no per-site IT negotiation and no carrier contract, and no second device or part-number decision for the yards that sit in coverage gaps: the firmware sets a single [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) preference of `wifi-cell-ntn` (prefer WiFi, fall back to cellular, fall back again to Skylo satellite) and the failover is handled inside the Notecard — the host firmware never branches on which network is live. Skylo's geostationary (GEO) network provides good coverage across open-sky terrain, which is exactly what a hive stand in an open orchard or meadow provides. **Skylo coverage is geography- and service-region-dependent; verify current coverage for your deployment region and review the [satellite best practices guide](https://dev.blues.io/starnote/satellite-best-practices/) before relying on satellite as the sole backhaul.** The combination of low-power cellular with automatic satellite failover is not a belt-and-suspenders add-on here; it is the architecture that makes this deployable in the real operating environment of commercial apiculture.
-
-
-
-**Deployment scenario.** The monitor lives in a small weatherproof enclosure zip-tied or screwed to the side of the hive stand. A 5 W solar panel on a short gooseneck bracket feeds a Li-ion battery through a solar MPPT charger. The load cell platform slides under the hive. Two sensor probes — one for temperature/humidity in the brood box, one for the microphone — thread through existing seams or ventilation holes. No modifications to the hive body are required; the bees never know anything has changed.
-
-## 2. System Architecture
-
-
-
-
-**Device-side responsibilities.** Every 15 minutes the onboard Cygnet STM32L433 host on the Notecarrier CX wakes via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn), reads hive weight and brood-box temperature and humidity, evaluates three independent threshold rules, and folds the result into a running summary in RAM. Audio is a once-a-day affair, not a once-every-15-minutes affair — colony acoustic state evolves on the scale of hours, so the firmware captures a ~0.75-second snippet on the rollover wake itself, computes the ZCR, RMS, and peak amplitude on-device, and discards the raw samples. A tripped threshold queues an alert Note immediately; a normal day's summary goes to the Notecard for the next outbound sync. Between wakes the host is fully powered off — the Notecard's ATTN pin cuts the supply, and the battery sees essentially zero draw. Raw audio never leaves the device.
-
-**Notecard responsibilities.** Notecard for Skylo buffers each [Note](https://dev.blues.io/api-reference/glossary/#note) on its on-device flash queue and opens a network session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) outbound cadence. The `card.transport` `wifi-cell-ntn` preference set at boot decides the path automatically — cellular where a tower is in reach, Skylo satellite where the orchard sits beyond terrestrial coverage — so the beekeeper's daily summary still reaches Notehub whether the apiary has cellular bars or not, with no firmware branching on which radio is live. Alert Notes carry `sync:true` to bypass the outbound timer; the beekeeper hears about a weight-drop event in the same minute it crosses the threshold (minutes, when the unit is on satellite), not at the next scheduled flush. [Environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) pushed from Notehub retune thresholds and cadences in the field without anyone driving out to reflash firmware.
-
-**Notehub responsibilities.** Each unit's embedded global SIM gets the Notecard onto carrier cellular worldwide and delivers data to [Notehub](https://notehub.io) over the Internet, which ingests every event and applies the project's [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). Hive summaries and alerts arrive in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile), so the beekeeper's SMS endpoint and the long-term weight-trend store can be served from the same device without any filter logic in between.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other targets; route configuration is project-specific and out of scope here. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub).
-
-## 3. Technical Summary
-
-
-1. **Flash the firmware.** Open `firmware/apiary_hive_monitor/apiary_hive_monitor.ino` in Arduino IDE. Install dependencies: Blues Wireless Notecard, HX711 Arduino Library, Adafruit SHT31 (all via Library Manager). Replace `PRODUCT_UID` with your [Notehub](https://notehub.io) ProjectUID. Build with `arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/apiary_hive_monitor/apiary_hive_monitor.ino` and upload (the FQBN matches `firmware/apiary_hive_monitor/sketch.yaml`, so omitting `--fqbn` also works when invoked from the sketch directory).
-
-2. **Wire sensors.** Connect HX711 (D5, D6), SHT31-D (SDA, SCL), and MAX9814 (A0) to Notecarrier CX per §5. Seat Notecard for Skylo in the M.2 slot and connect its `MAIN` and `GPS` antennas (§5).
-
-3. **Validate power.** Use Mojo (bench only) to confirm ~15–40 mA active, ~100–400 µA asleep. Confirm temperature/humidity readings and audio ZCR in summaries before field deployment.
-
-4. **Commission in Notehub.** Create a project, claim the Notecard, set `hx711_calibration` with known-weight test (§9 procedure), and set environment variables at the fleet level. Deploy.
-
-**What you'll have:** One compact daily Note with hive weight (kg), temperature/humidity averages, and acoustic features (ZCR, RMS, peak). Immediate alerts for weight drops, temperature anomalies, or acoustic changes. Full deployment to off-grid sites without cellular infrastructure.
-
-**Payload example** — `hive_summary.qo` (daily, compact template):
-
-```json
-{
- "weight_kg": 42.3,
- "weight_delta": -0.8,
- "temp_c_avg": 34.7,
- "humidity_avg": 67.2,
- "zcr_avg": 724,
- "rms_avg": 0.12,
- "peak_avg": 0.38,
- "samples": 96,
- "batt_mv": 3842
-}
-```
-
-**Alert example** — `hive_alert.qo` (immediate, `sync:true`):
-
-```json
-{
- "alert": "weight_drop",
- "value1": 4.7,
- "value2": 37.6,
- "batt_mv": 3901
-}
-```
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "hive_summary.qo",
- "body": {
- "weight_kg": 42.3,
- "weight_delta": -0.8,
- "temp_c_avg": 34.7,
- "humidity_avg": 67.2,
- "zcr_avg": 724,
- "rms_avg": 0.12,
- "peak_avg": 0.38,
- "samples": 96,
- "batt_mv": 3842
- }
-}
-```
-
-## 4. Hardware Requirements
-
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) ([buy](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host MCU — no separate Swan needed. Provides A0–A5 analog inputs, SDA/SCL I2C, and digital GPIO pins sufficient for all three sensors. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) ([buy](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)) | 1 | One M.2 module carrying cellular (LTE-M / NB-IoT / GPRS, Quectel BG95-S5 modem), WiFi (Silicon Labs WFM200S), and Skylo satellite (NTN) radios. Seats into the Notecarrier CX M.2 slot. The firmware's `card.transport` `wifi-cell-ntn` setting makes it prefer cellular at in-coverage apiaries and fall back automatically to the Skylo satellite network at yards beyond cellular reach — no second device or part-number decision, and no separate satellite enclosure. The integrated prepaid global SIM means no carrier contract and no per-site IT negotiation. Skylo GEO coverage varies by geography and service region; verify coverage for your deployment area before relying on satellite as the sole backhaul (see [satellite best practices](https://dev.blues.io/starnote/satellite-best-practices/)). This design does not configure or transmit device location — the board's GPS/GNSS receiver is used by the satellite stack internally for timing and ephemeris, not as a user-facing location source. Requires the antennas below. |
-| M16 cable glands (nylon, IP68) | 2 | One for the sensor cable bundle and one for the antenna pigtail penetration in the main enclosure. |
-| [Blues Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) ([buy](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)) | 1 | **Bench commissioning only.** Coulomb counter for power-budget validation. Mounts inline on the Sunny Buddy load rail between the charger and the Notecarrier CX `+VBAT` pad; wired to Notecarrier CX Qwiic during bench testing. Must be removed from the field assembly before deployment. See §9 for usage. |
-| [SparkFun Load Cell Amplifier HX711 (SEN-13879)](https://www.sparkfun.com/products/13879) | 1 | 24-bit ADC front-end designed for Wheatstone-bridge load cells; interfaces to the Cygnet via two GPIO pins using bit-bang protocol. |
-| [Zemic H8C-C3-100KG single-ended shear-beam load cell](https://www.zemiceurope.com/media/Documentation/H8C_Datasheet.pdf) | 1 | IP67-rated alloy-steel single-ended shear-beam cell, 100 kg capacity, OIML C3 accuracy class, 4-wire Wheatstone bridge with 1 m cable. The H8C is normally one of 3–4 cells in a platform scale; for this single-cell POC it is mounted as a cantilever weigh module (fixed end bolted to a rigid bracket, free end carrying the load through a button or rocker pin). See §5 for the mount detail and §10 for the off-center loading limitations this introduces. Sized for a loaded double-brood Langstroth with supers (typically 40–80 kg). Available through [Zemicus USA](https://www.zemicusa.com/products/) and Zemic-authorized scale-supply distributors. |
-| [Adafruit Sensirion SHT31-D Temperature & Humidity Sensor (product #2857)](https://www.adafruit.com/product/2857) | 1 | I2C sensor for inside-brood-box temperature and humidity; rated for high-humidity operation. Connected to the Notecarrier CX I2C header pins (VIN, GND, SDA, SCL) via four female-to-female jumper wires — the Notecarrier CX dual 16-pin header uses 0.1″ pitch, which does not accept a Qwiic/STEMMA QT connector directly. Brood temperature must stay 34–35 °C for healthy brood development — this is the most sensitive single health indicator in the hive. |
-| [Adafruit Electret Microphone Amplifier MAX9814 (product #1713)](https://www.adafruit.com/product/1713) | 1 | Analog amplified electret microphone with automatic gain control (AGC). Output connects to A0 on the Notecarrier CX for ADC sampling. The MAX9814's AGC keeps the signal in range across the wide amplitude variation between a quiet winter cluster and an agitated defensive colony. |
-| [SparkFun Sunny Buddy MPPT Solar Charger (PRT-12885)](https://www.sparkfun.com/products/12885) | 1 | Maximum power point tracking (MPPT) solar charger for single-cell Li-ion or LiPo (both chemistries share a 4.2 V CC/CV charge profile). Solar panel connects to the barrel-jack input (`IN+`/`IN−`); the battery connects to the `BATT` JST connector or `BAT+`/`GND` screw terminals; the `LOAD+`/`GND` output supplies the downstream device rail (Notecarrier CX via Mojo on the bench, or directly in the field). |
-| [Adafruit Lithium Ion Battery Pack 3.7V 4400mAh (#354)](https://www.adafruit.com/product/354) | 1 | Energy buffer for overnight and cloudy-day operation. The pack is two 18650-format Li-ion cells in parallel with integrated protection; the form factor and chemistry both differ from a LiPo pouch and the thermal-limit guidance in §10 is written specifically for Li-ion. At the nominal 15-minute sample cadence with the sleep-dominant firmware profile, estimated **active-phase** daily consumption is roughly 12–18 mAh (96 wakeup cycles × ~10 seconds at ~40 mA, plus one daily LTE Cat-1 bis sync at ~200 mA × ~60 seconds). The sleeping assembly also draws approximately 100–400 µA quiescent (Notecard low-power idle plus Notecarrier CX regulator and Sunny Buddy quiescent. See §9 power table), adding roughly 2.4–9.6 mAh/day. The whole-system daily budget is therefore approximately **14–28 mAh/day**, giving a 4 400 mAh pack at 80 % depth of discharge (3 520 mAh usable) on the order of **125–250 days of reserve** without any solar contribution. Actual reserve depends on NTN session frequency, cellular signal quality, and ambient temperature effects on Li-ion capacity — validate with Mojo during bench commissioning. Includes a JST-PH 2-pin connector that mates directly with the Sunny Buddy `BATT` JST port. |
-| [Voltaic Systems P105 5W 6V ETFE Solar Panel](https://voltaicsystems.com/5-watt-panel-etfe/) | 1 | 5 W, 6 V (peak 6.12 V / 940 mA) monocrystalline ETFE panel rated for long-term outdoor use. Ships with a 3.5 mm × 1.1 mm DC plug that inserts directly into the Sunny Buddy's barrel-jack solar input. At 940 mA peak, a full recharge of the 4400 mAh pack takes under 5 hours of direct sun. |
-| [Hammond 1554F2GY polycarbonate enclosure (IP66, 120 × 90 × 60 mm)](https://www.hammfg.com/product/1554F2GY) | 1 | IP66 polycarbonate enclosure with opaque lid, sized to house the Notecarrier CX, Sunny Buddy, and Li-ion pack. Drill two M16 cable-gland holes: one for the sensor cable bundle, one for the SMA antenna pigtail. The antenna mounts outside the enclosure (see antenna rows below), so the opaque lid does not affect the satellite or cellular link. Available from Hammond and authorized distributors. |
-| 4× female-to-female jumper wires, 150–200 mm (e.g. [Adafruit #266](https://www.adafruit.com/product/266)) | 1 set | Connect the SHT31-D breakout's 0.1″ header pins (VIN, GND, SDA, SCL) to the matching pins on the Notecarrier CX dual 16-pin header. |
-| STEMMA QT / Qwiic cable, 100 mm | 1 | Connects Mojo's Qwiic port to the Notecarrier CX Qwiic connector during bench commissioning so the coulomb-counter tally is readable from the blues.dev In-Browser Terminal; remove this cable before field deployment. |
-| Skylo-certified LTE/satellite antenna included with Notecard for Skylo (u.FL) | 1 | Connects to the `MAIN` u.FL port and carries **both** the terrestrial cellular signal and the Skylo satellite link — a single antenna for both networks. Use only the Skylo-certified antenna supplied with Notecard for Skylo; substituting an uncertified antenna risks regulatory non-compliance and link failure. Mount it outdoors on the enclosure lid or hive-stand upright with an unobstructed view of the sky (northern hemisphere: the southern sky toward the equator, where Skylo's GEO satellites sit), routed through a liquid-tight fitting. For an external SMA mount instead of the bare u.FL antenna, add a u.FL-to-SMA-F bulkhead pigtail (e.g. [Adafruit #851](https://www.adafruit.com/product/851)) through the enclosure wall. |
-| Passive GPS/GNSS antenna (u.FL) per the [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | 1 | Connects to the `GPS` u.FL port for GNSS time/ephemeris used internally by the satellite stack. Mount outdoors with a clear sky view alongside the main antenna; route through a liquid-tight fitting. |
-
-**Bundled connectivity (Notecard for Skylo):** Ships with an active global SIM including 500 MB of cellular data and 10 years of service, **plus** 10 KB of bundled Skylo satellite data — no activation fees, no monthly subscription, and no separate satellite provider subscription. Minimizing inbound sync frequency conserves the bundled satellite allocation; see §2 and §7 for guidance.
-
-## 5. Wiring and Assembly
-
-
-
-
-The enclosure mounts on the hive stand and the bees never know it's there — three sensor leads thread through existing seams, the antenna lives outside on the lid, and a small solar panel feeds the whole thing from a gooseneck bracket nearby. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin headers, and Notecard for Skylo seats into the M.2 slot. Its `MAIN` u.FL port connects to the included Skylo-certified antenna — which carries both the cellular and satellite signals — and its `GPS` u.FL port connects to the passive GPS/GNSS antenna; both mount outside the box with an unobstructed view of the sky.
-
-**Power chain.** The Sunny Buddy MPPT charger sits between the solar panel and the Li-ion battery. The Li-ion pack connects to the Sunny Buddy's `BATT` JST connector (or `BAT+`/`GND` screw terminals). The system load is drawn from the Sunny Buddy's `LOAD+`/`GND` screw terminals; the Notecarrier CX `+VBAT` pad connects to `LOAD+` and `GND` connects to the common ground rail. During bench commissioning, Mojo sits inline on the positive load rail to measure downstream device current:
-
-- Solar panel DC plug → Sunny Buddy barrel jack (`IN+`/`IN−`)
-- Battery JST-PH → Sunny Buddy `BATT` JST connector (charges battery from solar via MPPT; battery negative is at Sunny Buddy `GND`, which is common with `LOAD GND` and `IN−` on the Sunny Buddy PCB)
-- Sunny Buddy `LOAD+` → Mojo `BAT` input → Mojo `LOAD` output → Notecarrier CX `+VBAT` *(bench only)*
-- Sunny Buddy `LOAD GND` → Mojo `GND` → Notecarrier CX `GND` *(bench: Mojo intercepts positive rail only; ground is unbroken and common across all three boards)*
-- Sunny Buddy `LOAD+` → Notecarrier CX `+VBAT` *(field deployment, remove Mojo)*
-- Sunny Buddy `LOAD GND` → Notecarrier CX `GND` *(field deployment, direct ground return)*
-
-Mojo in this position measures the current flowing from the Sunny Buddy's load rail to the Notecarrier — the total downstream device draw, not the battery charge/discharge current directly. Ground is never interrupted by Mojo; it passes through directly to both the Notecarrier CX and back to the Sunny Buddy.
-
-**HX711 load cell amplifier:**
-
-- HX711 `VCC` → Notecarrier CX `+3V3_OUT`
-- HX711 `GND` → Notecarrier CX `GND`
-- HX711 `DOUT` → Notecarrier CX `D5`
-- HX711 `PD_SCK` → Notecarrier CX `D6`
-- Load cell `E+` (excitation positive) → HX711 `E+`
-- Load cell `E−` (excitation negative) → HX711 `E−`
-- Load cell `A+` (signal positive) → HX711 `A+`
-- Load cell `A−` (signal negative) → HX711 `A−`
-
-Mount the H8C as a single-cell weigh module: bolt the **fixed end** (the end with the strain-gauge cable exit) to a rigid steel angle bracket secured to the base or pad block, leaving the **free end** unconstrained so it can deflect under load. Thread a loading button or rocker pin into the tapped hole on the free end and rest the hive stand on that loading point plus three outrigger anti-tip pads — the outriggers carry no load, they only prevent the platform from tipping. This converts a single-ended shear-beam, which is normally one of 3–4 cells in a platform scale, into a viable single-cell hive scale at the cost of the off-center loading errors documented in §10. Follow the orientation and bolt-torque values given in the H8C datasheet — installing the cell upside-down or with the load applied on the wrong end produces incorrect readings and can damage the gauges. Apply a thin bead of RTV silicone sealant around the cable exit to keep moisture out.
-
-**SHT31-D temperature and humidity sensor:**
-
-The Notecarrier CX dual 16-pin header uses 0.1″ pitch and does not accept Qwiic/STEMMA QT connectors directly. Use four female-to-female jumper wires to the SHT31-D breakout's 0.1″ through-hole pads:
-
-- SHT31-D `VIN` → Notecarrier CX `+3V3_OUT`
-- SHT31-D `GND` → Notecarrier CX `GND`
-- SHT31-D `SDA` → Notecarrier CX `SDA`
-- SHT31-D `SCL` → Notecarrier CX `SCL`
-
-The SHT31-D address defaults to 0x44; it coexists on the I2C bus with the Notecard without conflict.
-
-- **POC caveat — in-hive placement.** The Adafruit SHT31-D breakout is not a sealed probe; it is an exposed PCB with a bare humidity membrane. The brood box interior runs at 34–36 °C and ≥ 85 % RH year-round, and propolis will accumulate on the PCB body over time. The Sensirion SHT31 sensor element itself is rated for high-humidity operation, but the breakout's solder joints and traces are not hardened for indefinite in-hive service. For short-term POC validation, thread the board in through an existing seam or ventilation slot, position the sensor head between frames 2 and 3 at mid-frame height, and coat the PCB body (not the sensing element) with beeswax or conformal coating to slow propolis adhesion. For production deployments, use a sensor with a sealed stainless probe tip rather than an exposed breakout board, and pot all external cable exits with marine-grade silicone or epoxy.
-
-**MAX9814 microphone:**
-
-- MAX9814 `Vin` → Notecarrier CX `+3V3_OUT`
-- MAX9814 `GND` → Notecarrier CX `GND`
-- MAX9814 `Out` → Notecarrier CX `A0`
-- Leave the MAX9814 `Gain` pin floating for the default 60 dB gain setting (adjustable to 50 or 40 dB with resistors if the AGC saturates in high-traffic hives).
-- **POC caveat — microphone placement.** The Adafruit MAX9814 breakout is an unenclosed PCB with an exposed electret capsule; it is not hardened for in-hive service. Mount the breakout board outside or at the edge of the hive entrance — close enough to pick up colony sound through the landing board gap — rather than placing it fully inside the hive body. The combination of 85 %+ RH, hive acids, and propolis buildup on and around the capsule opening will progressively attenuate the acoustic response; treating this as a long-term maintenance-free installation is not supported. For anything beyond short-term bench validation, apply conformal coating to the PCB (masking the capsule mesh opening) and thread only a thin wiring lead through the seam. If the application requires acoustic sensing from deep inside the brood box, use a sealed MEMS microphone module rated for high-humidity environments.
-
-**Analog reference (`+V_AREF` pin):** Leave unconnected; the STM32L433 ADC uses the internal 3.3 V reference for the A0 microphone input.
-
-**Antennas (single board, cellular + satellite + GPS):**
-
-Notecard for Skylo's `MAIN` u.FL antenna carries **both** the cellular and the Skylo satellite signal — one antenna replaces what used to be a separate cellular whip and an external satellite module's antenna.
-
-1. Connect the included Skylo-certified antenna's u.FL connector to the Notecard's `MAIN` u.FL port. The connector orients with the cable center-pin facing the port — it clicks firmly when fully seated. Use only the Skylo-certified antenna supplied with the board; an uncertified substitute risks regulatory non-compliance and link failure.
-2. Connect the passive GPS/GNSS antenna's u.FL connector to the Notecard's `GPS` u.FL port.
-3. Mount both antennas outside the enclosure with an unobstructed view of the sky. For satellite operation in the northern hemisphere, give the main antenna a clear view of the southern sky (toward the equator), where Skylo's GEO satellites sit; tilt and orient to maximize sky view. Route each through an M16 cable-gland port in the enclosure wall, or use a u.FL-to-SMA-F bulkhead pigtail (e.g. Adafruit #851) if you prefer an external SMA-mount antenna, securing the bulkhead with its lock nut from outside.
-
-Because the satellite link uses the same `MAIN` antenna as cellular, the placement that gives a good cellular signal also enables satellite fallback — there is no second module to mount and no JST cable to run. The opaque Hammond lid does not matter: both antennas live outside the box.
-
-**Mojo bench connection (remove before field deployment).** During bench commissioning, connect the second STEMMA QT / Qwiic cable from Mojo's Qwiic port to the Notecarrier CX Qwiic connector. The Notecard can then report the Mojo coulomb-counter tally (cumulative mAh, charge/discharge rate) in response to `card.power` requests issued from the blues.dev In-Browser Terminal. Remove the Mojo and Qwiic cable before sealing the field enclosure — the production firmware does not query Mojo in the deployed build.
-
-## 6. Notehub Setup
-
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/apiary_hive_monitor/apiary_hive_monitor.ino` as `PRODUCT_UID`.
-
-2. **Claim the Notecard.** Power up the unit at a location with cellular (or WiFi) coverage. Notecard for Skylo associates with your project on its first non-NTN sync. **This initial cellular/WiFi sync is mandatory before satellite works:** it registers the device with Notehub, registers the Notefile templates, downloads the current satellite ephemeris, and sets device time — all required before any NTN transmission will succeed. Commission each unit where it has terrestrial coverage even if it will routinely operate over satellite. See the [satellite best practices guide](https://dev.blues.io/starnote/satellite-best-practices/).
-
- **First-boot timing Note.** The firmware anchors the summary window to the first successful `card.time` response — the moment the Notecard returns a valid epoch rather than an error. Any sensor readings accumulated before that time anchor are discarded, and the window clock starts from that point. The first `hive_summary.qo` Note therefore arrives after a full `report_interval_hr` window has elapsed **from the first successful time sync** (default: 24 hours from that anchor), which may be later than first power-up if the unit is commissioned indoors or in a weak-signal location before cellular or satellite lock is achieved. This is intentional — a summary sent before a valid time reference would contain at most a single sensor reading and no meaningful weight delta.
-
-3. **Set a fleet per apiary yard.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. One fleet per yard lets you apply threshold environment variables at the yard level (all hives in a given orchard see similar ambient conditions) while overriding per-device for any hive that's known to be a heavy producer or an established swarm catcher.
-
-4. **Set environment variables.** In Notehub, navigate **Fleet → Environment** and set any values below (all are optional; firmware defaults are shown in the table). Any value set at fleet or device level overrides the compile-time default on the device's next inbound sync. Use [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) to automate fleet assignment based on device metadata.
-
- > **Commissioning Note — inbound cadence.** The firmware configures `inbound: 10080` (one week) to minimize satellite session costs. This means env var changes pushed from Notehub will not be pulled by the device until the next scheduled inbound, which may be up to seven days away. During bench commissioning, **force an immediate inbound sync** after each env var change by sending `{"req":"hub.sync"}` from the **blues.dev In-Browser Terminal** (upper right of the Notehub console). Alternatively, temporarily shorten the inbound interval for the bench session by sending `{"req":"hub.set","mode":"periodic","outbound":1440,"inbound":15}` from the terminal, and restore the weekly inbound (`"inbound":10080`) before field deployment.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_min` | `15` | Minutes between sensor readings and threshold evaluations. |
- | `report_interval_hr` | `24` | Hours between summary Notes. Changing this value also adjusts the Notecard's outbound sync cadence (`outbound = report_interval_hr × 60` minutes): the firmware reissues `hub.set` automatically on the next wake cycle after the env var is applied, so cellular and NTN transmission cost scales with summary frequency. |
- | `weight_alert_kg_drop` | `2.0` | Weight loss (kg) over the **current summary window** above which a `weight_drop` alert fires. Because the weight baseline (`weight_first_kg`) resets at the start of every summary window, this threshold applies over whatever `report_interval_hr` is set to — if you change the report interval to 12 hours, it becomes a 12-hour loss threshold. Tune per hive — a productive colony naturally loses weight on cool rainy days. |
- | `temp_low_c` | `32.0` | Brood-box temperature (°C) below which `temp_anomaly` fires. Below 32 °C brood viability drops rapidly. |
- | `temp_high_c` | `36.0` | Brood-box temperature (°C) above which `temp_anomaly` fires. Above 36 °C the colony is likely overheating or the sensor has shifted position. |
- | `audio_zcr_alert` | `1200` | Zero-crossing rate (counts per second) above which `audio_anomaly` fires. Elevated ZCR indicates a behavioral change from the colony's normal acoustic pattern; the specific cause requires physical inspection to determine. Baseline is typically 600–800 for calm, settled colonies, but varies by strain, season, and ambient noise environment. |
- | `hx711_calibration` | `2280.0` | HX711 scale factor (raw ADC counts per kg). Set this first using the known-weight procedure in §9. |
- | `hx711_zero_offset_kg` | `0.0` | Platform tare offset in kg. After calibrating the scale factor, set this to the reading reported by the empty platform (hive stand structure with no hive body). The firmware subtracts this value from every weight reading. See the tare procedure in §9. |
- | `reset_state` | (not set) | Set to `1` and force an inbound sync to immediately clear all accumulated summary-window data and restart the time anchor on the next wake. The firmware will discard any partial window in progress and begin a fresh accumulation cycle from the point the next valid `card.time` response is received. The reset is **one-shot**: the firmware records the current value in persisted state, so subsequent wakes that still read `reset_state=1` (which can continue for up to one inbound period, default one week — before Notehub delivers the cleared value) are silently skipped. You may leave `reset_state=1` in Notehub or remove it; either way the reset will not repeat. Setting the variable back to `0` or removing it and later setting it to `1` again will fire a second reset exactly once. Used during bench commissioning to start a known-clean calibration window. See §9. |
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) targeting `hive_alert.qo` (low-volume, route to SMS/email gateway or a CMMS endpoint) and a second targeting `hive_summary.qo` (daily, route to a long-term analytics store for weight trend analysis across the season). Keeping the Notefiles separate at the source lets you fan them to different destinations without filter logic inside the route.
-
-## 7. Firmware Design
-
-
-The firmware does almost nothing most of the time — which is the whole point. Each 15-minute wake reads three sensors, folds the readings into a running summary, fires an alert if anything crossed a threshold, and goes back to sleep. The sketch is split across three files in the same directory so the wake-cycle logic stays separate from the sensor drivers and Notecard helpers:
-
-- [`firmware/apiary_hive_monitor/apiary_hive_monitor.ino`](firmware/apiary_hive_monitor/apiary_hive_monitor.ino) — main sketch; wakeup sequencing, accumulation, alert evaluation, and sleep.
-- [`firmware/apiary_hive_monitor/apiary_hive_monitor_helpers.h`](firmware/apiary_hive_monitor/apiary_hive_monitor_helpers.h) — shared `HiveState` struct, extern declarations, and function prototypes.
-- [`firmware/apiary_hive_monitor/apiary_hive_monitor_helpers.cpp`](firmware/apiary_hive_monitor/apiary_hive_monitor_helpers.cpp) — sensor drivers, Notecard helper functions, and summary/alert emission.
-
-To open in Arduino IDE: **File → Open** and navigate to `firmware/apiary_hive_monitor/apiary_hive_monitor.ino`. The IDE will pick up the helper files automatically because they share the same directory.
-
-**Dependencies:**
-
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)) — install via Arduino IDE Boards Manager.
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (the `note-arduino` library) — install via Arduino Library Manager. See [note-arduino releases](https://github.com/blues/note-arduino/releases) for available versions; review the changelog for any breaking API changes before upgrading.
-- [`HX711 Arduino Library`](https://github.com/bogde/HX711) by Bogdan Necula and Lukas Bachschwell — install via Library Manager.
-- [`Adafruit SHT31 Library`](https://github.com/adafruit/Adafruit_SHT31) — install via Library Manager.
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard configuration (`hub.set`, `card.transport` `wifi-cell-ntn` for automatic cellular→Skylo satellite fallback, disable accelerometer; no location configured, the board's GPS receiver is used internally by the satellite stack only) | `notecardConfigure()` |
-| Note template definitions (compact, satellite-safe) | `defineTemplates()` |
-| Environment variable fetch and clamping | `fetchEnvOverrides()` |
-| HX711 10-sample weight average | `readWeightKg()` |
-| SHT31-D I2C temperature and humidity | `readTempHumidity()` |
-| Streaming audio feature extraction (ZCR, RMS, peak) | `readAudioFeatures()` |
-| Threshold evaluation and immediate alert emission | `sendAlert()` |
-| Daily aggregated summary emission | `sendSummary()` |
-| State persistence and sleep via ATTN | `setup()` → `NotePayloadSaveAndSleep()` |
-
-### Sensor reading strategy
-
-**Weight.** The HX711 is woken by pulling the PD_SCK pin low, then 10 successive readings are taken and averaged. The HX711 library handles the bit-bang timing internally. A calibration factor (`hx711_calibration` env var) converts raw 24-bit ADC counts to kilograms. After the read the HX711 is put to sleep (PD_SCK held high) to eliminate its ~1.5 mA idle draw during the sleep cycle.
-
-**Temperature and humidity.** A single SHT31-D measurement takes roughly 15 milliseconds; the library manages the I2C transaction and CRC check internally. Values are accumulated into running sums for the daily average; if the I2C read fails (returns NaN), the sample is excluded from the sum so a wired-off sensor does not bias the average to zero.
-
-**Audio feature extraction.** Audio is captured **once per summary window** (by default, once per day). The firmware checks the `audio_sampled` flag after evaluating window expiry, not before. On a successful rollover the flag is cleared before the audio check fires, so audio is captured on the rollover wake itself, making that wake the true first wake of the new window for all three sensor paths. On all subsequent wakes of the same window `audio_sampled` is set and the microphone is not re-sampled. On a failed rollover (`sendSummary()` returned false), `audio_sampled` is still true from the previous window and the block is skipped — consistent with the frozen-snapshot retry behavior for weight and temperature.
-
-When audio is attempted, `readAudioFeatures()` runs 12 consecutive 256-sample windows on A0 at approximately 4 kHz (`AUDIO_SAMPLE_PERIOD_US = 225 µs`), giving a ~0.75-second snippet total. Features are computed without storing the full audio buffer — which would require 32 KB at 16-bit resolution, a meaningful fraction of the Cygnet's 64 KB SRAM. `readAudioFeatures()` returns a validity flag: if the signal is implausible (DC offset outside the expected mid-rail band indicating a floating or disconnected A0, more than 30 % of samples within 32 LSB of the ADC rail indicating clipping, or normalized RMS below a minimum threshold indicating a dead or shorted microphone), the read is rejected. On an invalid read, `audio_anomaly` is not evaluated, `audio_sampled` is still set to prevent per-wake retries, and `zcr_avg`, `rms_avg`, and `peak_avg` emit `−9999` — the same sentinel convention used for weight timeouts and temperature NaN reads. Only the three computed features are stored or transmitted; no raw audio is recorded.
-
-- **Zero-crossing rate (ZCR):** Count of sign changes per second. Higher ZCR = higher frequency content = more active colony sound.
-- **RMS energy:** Root-mean-square of the windowed samples, normalized to the 12-bit ADC range. Tracks overall hive activity level.
-- **Peak amplitude:** Normalized maximum sample value in the window.
-
-These three features are averaged across the 12 windows and stored in the daily accumulator. The ZCR is the primary anomaly indicator; RMS and peak are supplemental context in the daily summary payload.
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles using `"format": "compact"` — required for Skylo NTN (satellite) operation and good practice regardless, as compact templates reduce on-wire payload size by 3–5× compared with free-form JSON, keeping each Note well within the 256-byte NTN payload ceiling.
-
-`hive_summary.qo` (daily, templated compact):
-
-```json
-{
- "file": "hive_summary.qo",
- "body": {
- "weight_kg": 42.3,
- "weight_delta": -0.8,
- "temp_c_avg": 34.7,
- "humidity_avg": 67.2,
- "zcr_avg": 724,
- "rms_avg": 0.12,
- "peak_avg": 0.38,
- "samples": 96,
- "batt_mv": 3842
- }
-}
-```
-
-`hive_alert.qo` (immediate, `sync:true`, templated compact):
-
-```json
-{
- "file": "hive_alert.qo",
- "body": {
- "alert": "weight_drop",
- "value1": 4.7,
- "value2": 37.6,
- "batt_mv": 3901
- },
- "sync": true
-}
-```
-
-The `value1` and `value2` fields carry alert-specific data: for `weight_drop`, the daily loss in kg and the current hive weight; for `temp_anomaly`, the current temperature and humidity; for `audio_anomaly`, the ZCR and RMS values. A single compact template handles all three alert types.
-
-### Low-power strategy
-
-After each 15-minute sample cycle, the host issues `NotePayloadSaveAndSleep()`, which serializes the runtime state (`HiveState` struct containing summary-window accumulators, last-alert timestamps, the weight baseline, and the `audio_sampled` flag) into the Notecard's flash memory, then arms [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to cut host power entirely for `sample_interval_min × 60` seconds. Between cellular sessions the Notecard enters its published low-power idle state (~8–20 µA per the Notecard datasheet). The HX711 is put to sleep via its PD_SCK pin after each weight reading. The microphone's AGC circuit draws from the 3.3 V rail only while the Cygnet host is awake (~5–8 seconds per cycle), and audio sampling itself occurs on only one of the 96 daily wake cycles.
-
-`notecardConfigure()` also issues a one-time [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) `{"method":"wifi-cell-ntn"}` on first boot so Notecard for Skylo prefers WiFi, then cellular, then Skylo satellite — the fallback is enabled at the Notecard, with no firmware branching on which radio is live (NTN is not enabled by default, so this call is what makes satellite fallback possible). The Notecard persists the setting in its own flash, so issuing it once is sufficient.
-
-The initial sync cadence is set to `outbound: 1440` (daily) to minimize satellite transmission costs at sites that operate over NTN; `sync:true` on alert Notes bypasses this timer and forces an immediate cellular or NTN session. `inbound` is set to `10080` (one week) per the [satellite best practices guide](https://dev.blues.io/starnote/satellite-best-practices/), since each inbound satellite ping costs approximately 50 bytes of the bundled 10 KB satellite allocation. Environment variable changes are applied on the next scheduled inbound sync.
-
-When `report_interval_hr` is changed in Notehub, the firmware automatically reissues `hub.set` with `outbound = report_interval_hr × 60` on the next wake cycle after the env var is picked up, keeping the Notecard's transmission schedule aligned with the local summary cadence. Changing `report_interval_hr` to `12` (for example) therefore both shortens the summary window and reduces the outbound sync interval to 720 minutes on the same wake cycle — no manual `hub.set` intervention required.
-
-### Retry and error handling
-
-- The first Notecard transaction — `hub.set` inside `notecardConfigure()` — runs a manual 5-attempt `requestAndResponse()` loop with a 1-second inter-attempt delay. Using `requestAndResponse()` rather than the higher-level `sendRequestWithRetry()` lets the code inspect the Notecard's `err` response field before treating the call as successful. `first_boot` is cleared only when the Notecard confirms `hub.set` was accepted without an error, not merely when the I2C transport completed, so a configuration that was rejected or silently dropped does not permanently skip setup.
-- SHT31-D reads that return NaN are excluded from daily temperature and humidity averages. The firmware maintains a separate `temp_valid_count` for successful SHT31-D reads so that failed reads do not bias the average toward zero. If `temp_valid_count` is zero at summary time (every read failed), `sendSummary()` calls `safeAvg()` which emits `−9999` as a sentinel, allowing downstream analytics to distinguish a total sensor failure from a near-zero valid reading.
-- HX711 reads that timeout (no data-ready pulse within 500 milliseconds) return `−1.0` and are excluded from the daily weight average.
-- Alert cooldown (`ALERT_COOLDOWN_MIN`, default 60 minutes) prevents a slowly drifting sensor from issuing a new page every 15 minutes until the threshold is corrected or the condition resolves.
-- Audio reads that fail hardware-plausibility checks are rejected entirely: `readAudioFeatures()` returns `false` when the DC offset is outside the expected mid-rail band (a floating or disconnected A0 input reads near 0 or 4095), when more than 30 % of samples in a window fall within 32 LSB of the ADC rail (railed or severely clipping input), or when the normalized RMS falls below the minimum signal threshold (dead or shorted microphone capsule). On an invalid read `audio_anomaly` is not evaluated, `audio_sampled` is still set to prevent per-wake retries for the rest of the window, and `zcr_avg`, `rms_avg`, and `peak_avg` emit `−9999` in the summary — the same sentinel convention used for weight timeouts (`−1.0` read → excluded, `safeAvg` emits `−9999`) and temperature NaN reads. A persistent `−9999` across consecutive daily summaries indicates a hardware failure (disconnected wire, failed capsule, or corroded contact) requiring physical inspection.
-
-### Key code snippet 1: compact template definition
-
-The `"format": "compact"` field is required for Skylo NTN (satellite) operation and cuts the per-Note wire size by 3–5× (e.g., a 200-byte JSON Note becomes ~40–80 bytes in compact form). `port` must be in the range 1–100. The numeric format codes (e.g., `14.1`, `12`) describe the wire encoding: `14.1` means a 4-byte IEEE 754 float, `12` means a 2-byte signed integer. The Notecard handles encoding/decoding automatically; no manual work required.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "hive_summary.qo");
-JAddNumberToObject(req, "port", 10);
-JAddStringToObject(req, "format", "compact");
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "weight_kg", 14.1); // 4-byte IEEE 754 float
-JAddNumberToObject(body, "weight_delta", 14.1);
-JAddNumberToObject(body, "temp_c_avg", 14.1);
-JAddNumberToObject(body, "humidity_avg", 14.1);
-JAddNumberToObject(body, "zcr_avg", 12); // 2-byte signed integer
-JAddNumberToObject(body, "rms_avg", 14.1);
-JAddNumberToObject(body, "peak_avg", 14.1);
-JAddNumberToObject(body, "samples", 12);
-JAddNumberToObject(body, "batt_mv", 12);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 2: two-pass streaming audio feature extraction
-
-Each 256-sample window is processed in two passes so that the per-window DC operating point is computed from the actual samples rather than assumed to be the ADC midpoint (2048). The MAX9814's AGC shifts the quiescent DC level across installations; measuring it directly makes the ZCR threshold portable without per-board recalibration. ZCR is also derived from the measured wall-clock window duration (`t1 − t0`) rather than a nominal sample rate, so it is accurate regardless of MCU speed or `analogRead()` latency.
-
-```cpp
-analogReadResolution(12); // 0–4095 range; 2048 is the normalization half-range
-
-// Pass 1: collect raw samples and sum for DC mean; bracket with micros() for
-// accurate ZCR (counts/second derived from actual elapsed time, not nominal rate)
-int16_t samples[AUDIO_WINDOW_SAMPLES];
-int32_t dc_sum = 0;
-unsigned long t0 = micros();
-samples[0] = (int16_t)analogRead(PIN_MIC);
-dc_sum = samples[0];
-for (int i = 1; i < AUDIO_WINDOW_SAMPLES; i++) {
- delayMicroseconds(AUDIO_SAMPLE_PERIOD_US);
- samples[i] = (int16_t)analogRead(PIN_MIC);
- dc_sum += samples[i];
-}
-unsigned long t1 = micros();
-int32_t dc_offset = dc_sum / AUDIO_WINDOW_SAMPLES; // per-window DC mean
-float dur_s = (float)(t1 - t0) * 1e-6f; // actual window duration
-
-// Pass 2: subtract DC mean, count sign changes, accumulate RMS / peak
-uint32_t crossings = 0;
-float sum_sq = 0.0f, peak_abs = 0.0f;
-int32_t prev = (int32_t)samples[0] - dc_offset;
-for (int i = 1; i < AUDIO_WINDOW_SAMPLES; i++) {
- int32_t cur = (int32_t)samples[i] - dc_offset;
- if ((prev < 0 && cur >= 0) || (prev >= 0 && cur < 0)) crossings++;
- float norm = (float)cur / 2048.0f; // normalize to [-1, 1]
- sum_sq += norm * norm;
- if (norm < 0.0f) norm = -norm;
- if (norm > peak_abs) peak_abs = norm;
- prev = cur;
-}
-float zcr = (float)crossings / dur_s; // counts per second
-float rms = sqrtf(sum_sq / (float)(AUDIO_WINDOW_SAMPLES - 1));
-```
-
-These per-window results are averaged across all 12 windows (`AUDIO_NUM_WINDOWS`) to produce the `zcr_avg`, `rms_avg`, and `peak_avg` fields in `hive_summary.qo`.
-
-### Key code snippet 3: sleep with state persistence
-
-`NotePayloadSaveAndSleep` serializes the state struct into Notecard flash and arms the ATTN pin to cut host power for the specified interval. The next boot rehydrates the state via `NotePayloadRetrieveAfterSleep`.
-
-```cpp
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &st, sizeof(st));
-NotePayloadSaveAndSleep(&payload, (uint32_t)sampleMin * 60, NULL);
-// Unreachable if ATTN pin is wired; fallback if not
-delay((uint32_t)sampleMin * 60000UL);
-```
-
-## 8. Data Flow
-
-
-
-
-**Collected.** Every 15 minutes: hive weight in kg (10-sample HX711 average) and brood-box temperature (°C) and relative humidity (%). Once per summary window (default daily, on the rollover wake): audio zero-crossing rate (counts/s), RMS energy, and peak amplitude from a ~0.75-second acoustic snapshot.
-
-**Accumulated.** Readings are summed into per-metric accumulators in RAM (persisted through sleep via `NotePayloadSaveAndSleep`). No raw samples are queued to the Notecard — only aggregated values and alert conditions.
-
-**Transmitted.**
-
-- `hive_summary.qo` — one compact-template Note every `report_interval_hr` (default 24 hours). Each numeric field is the mean of its valid samples over the window; a field with zero valid reads emits `−9999`. The `weight_delta` field is the difference between the day's first and last valid weight reading, making swarm departure immediately visible in the summary even if no threshold alert fired.
-- `hive_alert.qo` — emitted immediately on a threshold trip, with `sync:true` to force an immediate cellular or NTN uplink. Each alert type is suppressed for `ALERT_COOLDOWN_MIN` (60 minutes) after the first firing.
-
-**Routed.** Both Notefiles land in Notehub and from there to whatever routes the project configures.
-
-**Triggers.** Three rules fire alerts:
-
-- `weight_drop` — weight loss ≥ `weight_alert_kg_drop` (default 2.0 kg) from the first valid reading of the **current summary window** to the most recent reading. Because the baseline resets with each summary, the effective threshold window equals `report_interval_hr`. Primary indicator for swarm departure (rapid, > 1 kg/hour), theft, or gradual starvation.
-- `temp_anomaly` — brood-box temperature outside the `temp_low_c`–`temp_high_c` band (default 32–36 °C). Indicates chilling brood (late fall cluster shrinkage), overheating (poor ventilation), or queen loss causing reduced cluster heat production.
-- `audio_anomaly` — audio ZCR exceeds `audio_zcr_alert` (default 1200 counts/s). An elevated ZCR indicates a measurable acoustic behavioral change from a settled colony's typical pattern. The specific biological cause — defensive arousal, a disrupted colony, or environmental noise — cannot be determined from ZCR alone and requires physical inspection. Sustained low ZCR on an otherwise healthy hive is not currently flagged; that would require a per-colony baseline model.
-
-## 9. Validation and Testing
-
-
-**Expected cadence in steady state.** A healthy, settled colony produces one `hive_summary.qo` Note per day and zero `hive_alert.qo` Notes. Expect a short tuning period after deployment to verify that weight, temperature, and audio thresholds are appropriate for the specific hive: a heavy honey super changes the weight baseline, an aggressive strain may have a higher natural ZCR, and early spring colonies run cooler than peak-season.
-
-**Bench validation sequence.** Before field deployment:
-
-
-
-**Key constraint for weight calibration.** `weight_kg` in `hive_summary.qo` is the **mean of all valid readings** accumulated across the entire summary window — it is not an instantaneous reading. The platform load must remain **constant and unchanged** for the full duration of every summary window used in calibration or tare. Changing `report_interval_hr` takes effect on the **next wake** after the env var is applied — the firmware re-evaluates the expiry test `now − last_report_epoch ≥ reportHr × 3600` every wake using the current env var value, so a shorter interval can cause the current window to expire immediately on that wake. Always start calibration from a known-clean window boundary using the `reset_state` env var (see step 1b below).
-
-
-
-1. **Scale-factor calibration.**
-
- a. **Shorten the window.** In Notehub, set `report_interval_hr` to `1`. Force an immediate inbound sync by sending `{"req":"hub.sync"}` from the blues.dev In-Browser Terminal (see §6 step 4; without this, the weekly `inbound: 10080` setting means the env var may not arrive for up to seven days). Wait for the Notecard to confirm the sync; the new interval takes effect on the next wake after the env var is applied.
-
- b. **Clear any partial window in progress.** The firmware restores persisted state across reboots via `NotePayloadRetrieveAfterSleep`, so a power-cycle alone does not reset the summary window. To start from a known-clean boundary: in Notehub set `reset_state` to `1` (see §6) and force an immediate inbound sync (`{"req":"hub.sync"}`). On the next wake the firmware clears all accumulators and sets `last_report_epoch = 0`; the window is re-anchored when the next valid `card.time` response arrives. The reset is one-shot — subsequent wakes that still read `reset_state=1` (before the next inbound sync delivers the updated value) are silently skipped by the firmware. After confirming that the first 1-hour summary has been received (indicating that a complete clean window elapsed), you may leave `reset_state=1` in Notehub or remove it; the window will not reset again.
-
- c. **Empty-platform window.** Place the empty load-cell platform (no hive body) on the load cell and leave it completely undisturbed. Wait for one complete 1-hour summary. Record `weight_kg` from that Note — call it `platform_reading_kg`.
-
- d. **Known-mass window.** Place a known reference mass (for example, a 10 kg bag of flour or a calibrated test weight) on the platform and leave it completely undisturbed for one full window. Record the resulting `weight_kg` as `reported_weight_kg`.
-
- e. **Compute the corrected scale factor:**
-
- ```
- new_hx711_calibration = old_calibration × (reported_weight_kg − platform_reading_kg) / known_weight_kg
- ```
-
- The HX711 library computes `get_units() = raw_counts / scale`, so increasing the scale factor lowers the reported value and vice versa: if the reported net weight is **too high**, the ratio exceeds 1 and `new_calibration` increases (brings the output down); if too low, the ratio is < 1 and `new_calibration` decreases (brings the output up).
-
- **Example:** `old_calibration = 2280`, `platform_reading_kg = 0.3`, `reported_weight_kg = 13.3` with 10 kg reference → net reported = 13.0 kg (30 % high) → `new_calibration = 2280 × 13.0 / 10.0 = 2964`. Set `hx711_calibration = 2964` in Notehub, force an inbound sync, then set `reset_state = 1` and force a second inbound sync to discard the current partial window (which contains readings taken under the old calibration factor). Run one more full-window known-mass test with the platform load undisturbed to confirm the result is within ±0.5 kg before proceeding. The reset fires once; you may leave `reset_state=1` in place or remove it — no further action is needed to prevent repeated resets.
-
-2. **Platform tare (zero offset).**
-
- a. After scale-factor calibration is confirmed, remove the reference mass and leave only the empty stand platform undisturbed for one full summary window. Record `weight_kg` from that note — this is the platform dead-load.
-
- b. Set `hx711_zero_offset_kg` in Notehub to that value, force an inbound sync, then set `reset_state = 1` and force a second inbound sync to start a clean verification window. From this point, all `weight_kg` readings represent hive body and contents only (referenced to zero with the empty stand). Confirm the first full-window empty-platform summary reports approximately 0.0 kg (typically within ±0.2 kg for this load cell class). The reset fires once; you may leave `reset_state=1` in place or remove it — subsequent wakes will not repeat the reset.
-
- > **Important:** `hx711_zero_offset_kg` corrects only the static dead-load. The firmware does not compensate for load-cell temperature drift. Alloy-steel shear-beam cells drift slightly with temperature; for seasonal outdoor deployments, re-check the offset once per season or after significant ambient temperature changes.
-
- > **Relative vs. absolute weight:** If you skip the tare procedure, `weight_kg` includes the platform dead-load and does not represent true hive mass. The `weight_delta` field and the `weight_drop` alert are relative measurements that remain meaningful regardless of whether the tare offset is set.
-
-3. **Restore the production interval.** Set `report_interval_hr` back to `24` (or your target production value) in Notehub, force a final inbound sync, then set `reset_state = 1` and force one more inbound sync to start a clean first daily window. The firmware fires the reset once and then ignores subsequent `reset_state=1` readings — you may leave `reset_state=1` in Notehub or remove it before field deployment; either is safe and the device will not reset its window on the first weekly inbound sync.
-
-4. Breathe on the SHT31-D sensor and verify that the temperature and humidity fields rise in the next summary Note.
-
-5. Speak clearly into the microphone and verify that `zcr_avg`, `rms_avg`, and `peak_avg` appear in the summary. Because audio is captured only once per summary window, use serial debug output — with `DEBUG_SERIAL` enabled in `apiary_hive_monitor_helpers.h`, `[APP] Summary sent` prints the sample count and all three audio feature values, or rely on the shortened 1-hour `report_interval_hr` bench run to confirm the values quickly. Restore production defaults before field deployment.
-
-6. **Alert pipeline validation.** Two alert rules make reliable bench tests:
-
- - **Temperature path (instant, no additional equipment).** During indoor bench testing the SHT31-D reads ambient room temperature, typically 20–24 °C — which is already below the default `temp_low_c` threshold of 32 °C. A `temp_anomaly` alert will therefore fire on the first wake cycle after commissioning without any configuration change. Use this to confirm the full alert path (host → Notecard → Notehub → route) is functional before proceeding to field deployment. After the initial alert the 60-minute cooldown (`ALERT_COOLDOWN_MIN`) suppresses repeats; reset the device or wait out the cooldown for a second confirmation.
-
- - **Weight-drop path (physical mass removal required).** The `weight_drop` rule compares the **current** weight to `weight_first_kg` — the first valid reading of the current summary window, not to the previous 15-minute sample. Lowering `weight_alert_kg_drop` to a small value and waiting will not fire an alert if the platform mass has not actually changed since the window started. To test this rule deterministically: allow the unit to complete at least one full wake cycle so `weight_first_kg` is anchored; then **physically remove a calibrated mass from the platform that exceeds the configured threshold** (for example, remove a 2 kg test weight while `weight_alert_kg_drop` is at the production default of `2.0`). A `weight_drop` alert should appear in Notehub within the next 15-minute sample cycle. Replace the mass and confirm subsequent weight readings return to near the baseline before declaring the test complete.
-
-**Using Mojo to validate power behavior.** The Blues [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) coulomb counter sits inline between the Sunny Buddy `LOAD+` output and the Notecarrier CX `+VBAT` rail during bench commissioning. Connect a STEMMA QT / Qwiic cable from Mojo's Qwiic port to the Notecarrier CX Qwiic connector; the Notecard can then relay Mojo's cumulative mAh tally in response to `card.power` requests from the blues.dev In-Browser Terminal. Expected current draw by phase:
-
-| Phase | Figure | Source |
-|---|---|---|
-| Cygnet + sensors active, weight + temp cycle (no audio) | ~15–30 mA, ~5–8 s/cycle | Whole-device estimate |
-| Cygnet + sensors active, weight + temp + audio cycle | ~20–40 mA, ~6–9 s/cycle | Whole-device estimate (audio adds ~5–30 mA × 0.75 seconds) |
-| Notecard low-power idle (between sessions, host off) | ~8–20 µA | Notecard datasheet |
-| Cellular session (BG95-S5 modem) | ~250 mA avg; brief peaks up to ~2 A for a few ms on a 2G burst | Notecard datasheet |
-| Skylo NTN satellite session | ~250 mA avg (same magnitude as a cellular session) | Notecard datasheet |
-| Host cut via ATTN, Notecard low-power idle + Sunny Buddy quiescent | ~100–400 µA typical | Whole-device estimate |
-
-Notecard-published figures come from [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/); whole-device estimates include the Notecarrier CX regulator and Sunny Buddy quiescent draw and should be validated with Mojo on your specific assembly.
-
-The expected Mojo pattern for a correctly sleeping unit: a brief 5–8 second active spike every 15 minutes, and a longer cellular spike once every 24 hours. If instead you see continuous > 20 mA draw, the host is not sleeping — check that the `ATTN` pin is connected between the Notecard and the host (the Notecarrier CX routes this internally). If you see no cellular spike in 25 hours, confirm `hub.set` outbound cadence and that the device has associated with a Notehub project.
-
-Because the unit is solar-powered, the absolute mAh number matters less than the shape of the trace. The bench measurement is still valuable as a regression test: if a firmware change doubles the sleep-phase current, the Mojo trace makes it immediately visible before the unit goes to a remote yard.
-
-## 10. Troubleshooting
-
-
-| Symptom | Likely Cause | Check |
-|---------|-------------|-------|
-| No Notes appear in Notehub after 48 hours | Notecard not associated with project, or first boot config failed | Verify `PRODUCT_UID` is set correctly. Check Notehub console for any device events. Force a hub.sync from the terminal to trigger association. |
-| Continuous `temp_anomaly` alerts indoors | SHT31-D reading room temperature, which is below the default `temp_low_c = 32 °C` | This is expected during bench testing (alerts confirm the alert path works). Ignore or lower `temp_low_c` during commissioning. Alerts will cease once the unit is deployed in a warm hive. |
-| Weight readings are negative or too high | Calibration scale factor not set, or platform tare offset is wrong | Run the known-weight calibration procedure in §9 step 1. Confirm `hx711_calibration` and `hx711_zero_offset_kg` are set in Notehub environment variables. |
-| Weight doesn't change between summaries, or only changes erratically | Load cell disconnected, or HX711 powered down before sleep | Verify all four load cell wires (E+, E-, A+, A-) are connected to HX711. Check that D5/D6 GPIO pins are not shorted or pulled low continuously. Confirm the HX711 sleep command is not being called prematurely. |
-| Audio features always show -9999 in summaries | Microphone disconnected, amplifier unpowered, or MAX9814 output clipped/floating | Verify A0 connection from MAX9814 to Notecarrier CX. Check that MAX9814 Vin and GND are connected to 3.3 V rail. Speak clearly into the microphone during the audio sample window (occurs on the first wake of each summary window, visible in debug output if `DEBUG_SERIAL` is enabled). |
-| Mojo shows no current draw, or > 500 mA continuous | Host not waking (ATTN pin not connected or not toggling), or firmware stuck in a loop | Verify Notecard `ATTN` pin is wired to Notecarrier CX `ATTN` (routed internally on the CX). Check firmware compile-time configuration and DEBUG_SERIAL output. If the unit draws power continuously with no pattern, the host is not sleeping — stop and debug firmware. |
-| Satellite (NTN) never transmits, even with sync:true alert | Initial cellular/WiFi sync never completed, `card.transport` not set to an `ntn` method, or antenna/sky-view issue | The device must complete at least one successful cellular or WiFi sync (which registers templates and downloads ephemeris) before NTN is enabled, and `card.transport` must be `wifi-cell-ntn` (set on first boot by `notecardConfigure()`). Bring the unit into cellular coverage and force a `hub.sync` from Notehub. After the sync completes, alerts with `sync:true` will fall back to satellite if cellular is unavailable. Verify the Skylo-certified `MAIN` antenna is mounted outdoors with a clear view of the (southern, northern-hemisphere) sky. |
-| Device claims to Notehub but doesn't pull environment variables | Inbound sync cadence too long (default 10080 minutes = 1 week) | During commissioning, force an immediate inbound sync by sending `{"req":"hub.sync"}` from the [Blues In-Browser Terminal](https://dev.blues.io/terminal/). Environment variable changes are pulled on the next scheduled inbound or after a manual sync. See §6 step 4 for commissioning settings. |
-
-## 11. Limitations and Next Steps
-
-
-This reference design covers a single hive with a single load cell, exposed PCB-style sensors, and a heuristic acoustic threshold — appropriate for a POC, not yet ready for an unattended multi-year orchard deployment. The list below names the boundaries that come with that scope, starting with the Li-ion thermal limits that matter most for outdoor apiculture, and ending with the production paths that turn this into a fleet-grade hive scale.
-
-### Simplified for the POC
-
-The simplifications below are deliberate scope choices, starting with the Li-ion thermal limits that matter most for outdoor apiculture and ending with the single-hive boundary — each a place where a production deployment will add a safer battery, a richer model, or another sensor.
-
-**Li-ion battery thermal limits — safety and reliability.** Lithium-ion cells (the chemistry of the Adafruit #354 18650 pack specified in §4) **must not be charged below 0 °C** (lithium plating can cause an internal short) **or above approximately 45 °C** (accelerated cycle-life degradation and, at extremes, thermal runaway). The SparkFun Sunny Buddy (LT3652) does not use a battery-thermistor input in the default board configuration and therefore does not gate charging based on temperature. In practice this means:
- - **Freezing mornings:** The solar panel will attempt to charge the battery at ambient temperature. In apiaries that see sub-zero nights in spring or autumn, the charger will push current into a cold cell. At minimum this degrades capacity; in severe cases it creates a safety risk. A production deployment should use a charger with NTC thermistor supervision or add a simple low-temperature charge-inhibit circuit.
- - **Sun-heated enclosures:** A polycarbonate enclosure mounted on a south-facing hive stand in direct sun can easily reach 50–60 °C interior temperature on a summer afternoon, well above the safe charging ceiling. In production, use a metal enclosure with thermal mass, a sun shield or white enclosure finish, and validate temperatures with a data logger before long-term deployment.
- - **Capacity in cold weather:** Li-ion usable capacity drops to 60–80 % at 0 °C and further below freezing. The 4400 mAh reserve calculations in §4 assume room-temperature performance; reduce the estimated reserve accordingly for cold-climate apiaries.
- - **Production alternatives:** For year-round outdoor apiary deployment, consider a LiFePO₄ pack and charger/BMS combination that is specifically rated for your deployment climate. LiFePO₄ is more tolerant of elevated temperatures than Li-ion, but safe charging temperature limits vary substantially by specific chemistry, cell construction, and BMS design — many packs still prohibit charging below 0 °C without an integrated heater or a low-temperature-rated BMS. Do not apply chemistry-wide temperature numbers as a substitute for reading the datasheet of the pack you intend to use: select a battery pack and charger whose **rated** charge and discharge temperature windows cover the full seasonal temperature swing at your deployment site, and verify those limits in the manufacturer's datasheet before committing to a production build. Alternatively, use a primary lithium cell bank for winter-dormant hives where solar recharge is not required.
-
-**Audio analysis is heuristic, not ML-based.** Zero-crossing rate and RMS energy are established acoustic features used in beehive research, but the firmware applies simple static thresholds rather than a learned colony baseline. The `audio_anomaly` alert indicates a measurable behavioral change from a typical settled-colony acoustic signature; **it is not a diagnosis.** Possible causes include defensive arousal, post-swarm disruption, environmental noise (nearby equipment, traffic), or other disturbances — all of which require physical inspection to identify and distinguish. A colony that is naturally more vocal (Carniolan vs. Italian strains, hot weather, nearby traffic) may fire false positives; a colony that is genuinely quieter than the default threshold may never fire an alert even if conditions warrant attention. The `audio_zcr_alert` env var exists to tune this per-hive, but doing it well requires a few weeks of observation. More specific hypotheses about queenlessness, swarming pressure, or defensive state require richer acoustic features, a colony-specific learned baseline, and physical verification before any conclusion can be drawn.
-
-**Load cell calibration is manual; absolute weight requires a tare step.** The `hx711_calibration` scale factor and `hx711_zero_offset_kg` platform tare must both be set experimentally (see §9 for the procedure). Without the tare step the `weight_kg` field includes the platform dead-load and does not represent true hive mass; the relative fields (`weight_delta` and `weight_drop` alert) remain valid in both calibrated and uncalibrated states. The firmware does not implement temperature-compensation for the load cell — alloy-steel shear-beam cells drift slightly with seasonal temperature swings, which can introduce a small systematic offset over the year.
-
-**Single load cell, single-end weighing.** The design weighs the hive from one end, which is accurate if the load cell is centered under the stand. An uneven hive body (tilted entrance board, added super on one side) can introduce a systematic reading offset. A proper hive scale uses 4 corner cells with a Wheatstone bridge; this POC uses one cell for simplicity.
-
-**Satellite (NTN) operation caveats.** The same Notecard for Skylo runs the same firmware everywhere, but when it falls back to the satellite link the NTN path has operational characteristics to plan for. The device must complete at least one successful Notehub sync over cellular or WiFi before NTN transmissions will work — the Notecard needs registered templates, time, and location data to find satellites efficiently, so a unit deployed directly in a cellular dead zone will not transmit until it can be briefly brought into terrestrial coverage. Over satellite, a `sync:true` alert is prioritized for the next Skylo session but locating a satellite and completing transmission takes several minutes (not seconds); each Note must stay within the **NTN 256-byte maximum** (the compact templates here do); and each inbound env-var sync consumes ~50 bytes of the bundled 10 KB satellite allocation, which is why `inbound` defaults to weekly. The `MAIN` antenna must be outdoors with an unobstructed sky view (northern hemisphere: southern sky toward the equator) for the satellite link to work. Skylo coverage is region-dependent — verify coverage for your deployment area (see [satellite best practices](https://dev.blues.io/starnote/satellite-best-practices/)).
-
-**No audio recording.** The firmware computes features only; no raw audio is stored or transmitted. Diagnosing a specific anomaly from the cloud requires a follow-up physical inspection. The feature scores narrow down the likely cause but do not replace the beekeeper's judgment.
-
-**Mojo is bench-only in this POC.** The firmware does not read the Mojo's LTC2959 coulomb counter over I2C; it is used only as a bench instrument to validate power draw during development. Adding a `batt_mah` field to the daily summary is straightforward.
-
-**Single hive per device.** One Notecarrier CX manages one hive. A yard with 50 hives needs 50 units; a multi-drop I2C bus with individually addressed sensors and a single Notecard handling an entire yard is a reasonable production extension.
-
-### Production Next Steps
-
-Once a yard is running the basic hive scale, the following extensions turn it into a fleet-grade instrument — roughly from the most immediately useful accuracy gains toward the deepest analytics and fleet-update capabilities.
-
-**Per-colony audio baseline learning** records a rolling 30-day ZCR distribution per hive and alerts only on deviations beyond two standard deviations from that colony's own baseline, rather than a fixed threshold.
-
-**A multi-cell weight platform** — four corner cells in a full Wheatstone bridge — delivers platform-grade accuracy and eliminates tilt errors.
-
-**4-wire cable waterproofing** pots the load cell cable exit and all external sensor penetrations with marine-grade epoxy for years-long outdoor service.
-
-**A Varroa mite proxy** is within reach: some research groups have identified spectral signatures in the 100–300 Hz band correlated with high Varroa loads. Adding a narrow-band FFT bin to the audio features would surface this indicator without additional hardware.
-
-**Outboard DFU** via [Notecard Outboard Firmware Update](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) enables shipping new host firmware images to an entire yard fleet over cellular — bug fixes, improved audio scoring algorithms, new threshold logic, or support for additional sensors — without a truck roll to each hive. Operational thresholds and calibration values are already handled today through Notehub environment variables; ODFU addresses the separate need to update the compiled firmware logic itself.
-
-## 12. Summary
-
-
-The beekeeper who used to learn about a departed swarm three days after the fact now gets a daily weight-trend notification and an immediate alert the moment the hive loses 2 kg — wherever in the orchard, meadow, or forest edge the colony happens to live. Three sensors and a single Notecard for Skylo — cellular, WiFi, and Skylo satellite on one module — turn the hive stand into a continuously monitored asset, reaching cellular where there's a tower and the Skylo satellite network where there isn't, with nothing to swap. Sampling runs every 15 minutes, the summary goes out once a day, and threshold alerts bypass the transmit timer entirely. Long-lived solar operation is achievable on the energy budget described in §4 and §10, but only after the commissioning steps in §9 — Mojo power validation, load-cell calibration, and a battery chemistry rated for the deployment climate. For a commercial beekeeper managing dozens of yards across a region, that's the difference between a minor nuisance and a significant economic loss — exactly the gap that cellular-first IoT is designed to close.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/remote-apiary-hive-health-monitor/)**
diff --git a/66-remote-apiary-hive-health-monitor/banner.png b/66-remote-apiary-hive-health-monitor/banner.png
deleted file mode 100644
index 29e081cd..00000000
Binary files a/66-remote-apiary-hive-health-monitor/banner.png and /dev/null differ
diff --git a/66-remote-apiary-hive-health-monitor/diagrams/01-system-architecture.svg b/66-remote-apiary-hive-health-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index 021a56e9..00000000
--- a/66-remote-apiary-hive-health-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,118 +0,0 @@
-
diff --git a/66-remote-apiary-hive-health-monitor/diagrams/02-wiring-assembly.svg b/66-remote-apiary-hive-health-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 3c8f6feb..00000000
--- a/66-remote-apiary-hive-health-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,132 +0,0 @@
-
diff --git a/66-remote-apiary-hive-health-monitor/diagrams/03-data-flow.svg b/66-remote-apiary-hive-health-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index f68c096a..00000000
--- a/66-remote-apiary-hive-health-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/68-cnc-machine-spindle-load-cycle-time-tracker/README.md b/68-cnc-machine-spindle-load-cycle-time-tracker/README.md
index d172b152..821e85cc 100644
--- a/68-cnc-machine-spindle-load-cycle-time-tracker/README.md
+++ b/68-cnc-machine-spindle-load-cycle-time-tracker/README.md
@@ -1,461 +1,5 @@
-# CNC Machine Spindle Load & Cycle Time Tracker
+# CNC Machine Spindle Load and Cycle Time Tracker
-
+A cellular reference design for CNC machine-tool OEMs to monitor spindle load, cycle counts, idle time, and alarm codes in their installed base without touching the customer's plant network.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=CNC%20Machine%20Spindle%20Load%20%26%20Cycle%20Time%20Tracker).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/cnc_spindle_tracker/](firmware/cnc_spindle_tracker/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is an [industrial equipment monitoring](https://blues.com/industrial-equipment-monitoring/) reference design for machine-tool OEMs who want continuous visibility into how their installed base is actually being used. The device sits on the customer's CNC machine and reports the operational signals an OEM cares about: how hard the spindle is working, how many cycles ran each hour, how often the machine sits idle, which operator is logged in, and what alarm codes the controller raises. All of it travels back to the OEM's cloud over cellular, without ever touching the customer's plant network. Routine telemetry batches into hourly summaries; alarms and operator-ID changes arrive immediately. The hardware is an Arduino OPTA RS485 with a Blues Wireless for OPTA cellular expansion (see §4 for the BOM); the data source is any CNC controller that exposes telemetry over **Modbus TCP**.
-
-## 1. Project Overview
-
-**The problem.** A CNC (computer numerical control) machining center is a six-figure piece of capital equipment. The OEM who built it typically has zero visibility into what happens to it after delivery. Is the spindle running at 80% load twelve hours a day, or is it sitting idle because the shop scheduled it wrong? Is the same alarm code triggering every Tuesday morning, a pattern that, with data, is obviously a tooling-change reminder, but without data is an invisible warranty claim? Is a particular operator overriding the feed rate up to 140% on a finishing pass and burning through inserts?
-
-These questions matter to OEMs because the whole industry is moving toward **EaaS** (equipment as a service) and usage-based billing. You cannot sell spindle-hours if you don't know how many spindle-hours are actually running. You cannot offer a predictive-maintenance contract if you can't see the telemetry. And you cannot benchmark your machine's **OEE** (Overall Equipment Effectiveness, a manufacturing KPI that combines availability, performance, and quality into a single utilization score) without a continuous stream of cycle-time and run/idle data flowing back from the shop floor.
-
-The data is already there, on controls that expose it. A meaningful subset of modern CNC controls expose live spindle load, feed-rate override, alarm codes, cycle state, cumulative part counts, and current operator ID over a Modbus TCP interface on an Ethernet port on the back of the cabinet. Whether a specific controller supports this depends on the vendor, model, and which optional feature packages are installed — consult the controller's Modbus/Ethernet option documentation to confirm availability and the exact register layout before deploying. Some vendors instead use proprietary protocols or OPC-UA (Siemens SINUMERIK, Fanuc FOCAS-over-Ethernet) and require a protocol translation layer not covered here. Where Modbus TCP is available, the problem isn't where the data lives; it's getting that data off the machine and back to the OEM without involving the machine shop's network.
-
-**Why Notecard.** Machine shops run their CNC machines on isolated **OT** (operational technology) networks: air-gapped from the corporate LAN by design, and protected by a plant IT department that is deeply skeptical of anyone proposing to plug the OEM's monitoring device into their subnet. Asking every customer to configure a VLAN, assign a static routable IP, punch a firewall rule, and maintain the configuration through staff turnover is a customer-acquisition and support nightmare. The OEM's monitoring device needs to work identically, without any site IT involvement, whether it's deployed in a shop in Ohio, Stuttgart, or Osaka.
-
-
-
-Cellular is the answer. A [Blues Wireless for OPTA](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) snapped onto the Arduino OPTA's expansion port gives the device its own independent cellular data channel: a private uplink to the [Blues Notehub](https://blues.com/notehub/) cloud service that needs no plant LAN credentials, no firewall exception, and no IT ticket. The Modbus TCP connection to the CNC runs over a direct point-to-point Ethernet cable between the OPTA and the machine control: two devices on a private subnet with no routing to the shop floor network at all. The monitoring device is invisible to plant IT because it never touches plant IT's infrastructure.
-
-**Deployment scenario.** A single Arduino OPTA RS485 + Blues Wireless for OPTA mounts on the machine's electrical panel DIN rail. A two-meter Cat6 patch cable runs from the OPTA's RJ45 port directly to the CNC controller's Modbus TCP Ethernet port, a closed, private connection with its own subnet. The cellular antenna exits through a cable gland on the panel door. The panel's existing 24 VDC supply powers the assembly. First-light is an hour of wiring; the OEM's service technician never needs to interact with the shop's network team.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The OPTA's STM32H747 Cortex-M7 host is the Modbus TCP **client** in this relationship — once per minute it asks the CNC controller (the Modbus **server**) for the same block of six holding registers over the OPTA's built-in Ethernet port. Every poll feeds the rolling hourly statistics the OEM cares about (mean and peak spindle load, mean feed-rate override, run/idle minute counts, cycle completions, and average cycle time) and runs the two sample-based alert rules (`spindle_overload` and `cnc_alarm`), plus a third connection-failure alarm (`modbus_unreachable`) when the Modbus TCP link is still unreachable after a retry. Operator-ID transitions are caught on the same poll: any change in the operator-ID register fires an immediate `cnc_operator.qo` Note. Everything the host produces travels over I²C, through the expansion's AUX connector, to the Notecard inside Blues Wireless for OPTA. The host never touches the cellular modem or its session state.
-
-**Notecard responsibilities.** From there the Notecard takes over. It queues every [Note](https://dev.blues.io/api-reference/glossary/#note) on-device, brings up a cellular session on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes), and, for anything marked `sync:true`, wakes the radio inside a minute of the alert firing. It is also the channel for configuration coming the other way: [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) pushed from Notehub let OEM application engineers retune the Modbus register block base address, alert thresholds, and reporting cadence per-fleet without touching firmware. When `report_minutes` changes, the firmware reissues `hub.set` so the Notecard's outbound cadence stays aligned with the summary window.
-
-**Notehub responsibilities.** The Notecard manages its own cellular session against the supported carrier networks worldwide via its embedded global SIM, then hands the data off to [Notehub](https://notehub.io), which ingests every event, stores it, and applies project-level routes. [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) and [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) are the natural unit of organization for an OEM here: one fleet per controller family or model profile, carrying the register block base address, unit ID, port, and alert thresholds as fleet-level environment variables. Grouping by controller model rather than by customer site is the more useful axis, because register maps often differ across CNC models even within the same facility. IP addressing remains compile-time in this reference design, so machines on different network configurations still require a per-build adjustment (see [§12 Limitations](#12-limitations-and-next-steps)).
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP Pub/Sub, Snowflake, and several other targets; see the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). This project does not ship a specific downstream endpoint; the OEE dashboard is routed from Notehub as a project-specific integration.
-
-## 3. Technical Summary
-
-1. **Flash the Arduino OPTA** with `firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino` (requires Arduino IDE + Mbed OS Opta Boards core).
-2. **Get ProductUID** from [notehub.io](https://notehub.io), paste it into the sketch, reflash.
-3. **Wire**: Cat6 from OPTA RJ45 → CNC Modbus TCP port (default `192.168.250.1:502`). Cellular antenna through panel door.
-4. **Power**: 24 VDC to OPTA. Notecard auto-claims to your Notehub project on first cellular session (≈5 minutes).
-5. **Validate**: Check Notehub for `cnc_summary.qo` Notes within 60 minutes. One summary per hour, one alarm per event (when triggered).
-
-**When you're done:** you have continuous spindle load, cycle counts, alarm codes, operator IDs, and run/idle telemetry flowing to Notehub every hour, plus real-time cellular alarms on overload or fault transitions. Aggregate the summaries into an OEE dashboard; route alarms to your CMMS or on-call system via Notehub routes.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "cnc_summary.qo",
- "body": {
- "spindle_pct_mean": 71.4,
- "spindle_pct_peak": 88.2,
- "feed_override_pct_mean": 97.5,
- "run_min": 51,
- "idle_min": 9,
- "cycle_count": 25,
- "avg_cycle_sec": 122,
- "operator_id": 7,
- "alarm_count": 0,
- "valid_samples": 60
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Arduino OPTA RS485](https://store.arduino.cc/products/opta-rs485) | 1 | Industrial DIN-rail micro-PLC; built-in Ethernet RJ45 for Modbus TCP to CNC; RS-485 available as a future bus option; Cortex-M7 host with sufficient RAM for rolling stats. |
-| [Blues Wireless for OPTA (NA, SKU 992-00155-C)](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Snaps onto the OPTA's right-hand expansion port; adds a [Notecard Cell+WiFi](https://dev.blues.io/datasheets/notecard-datasheet/note-wbnaw/) over I²C with a prepaid global SIM. Cellular removes all plant-network dependencies. Select the EMEA variant ([SKU 992-00156-C](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)) for European deployments. |
-| External cellular antenna(s), SMA, ~3 m lead (e.g. [SparkFun CEL-16432](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html)) | 1 required, 2 recommended | Metal electrical panels block cellular signals. Route at least the primary antenna through a cable gland to the outside of the panel; add the diversity antenna for better LTE Cat-1 performance in low-signal environments. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Bench-only coulomb counter. Inline on the Wireless for OPTA power input during commissioning to validate the Notecard subsystem's per-session energy. Not deployed to the field. |
-| 24 VDC DIN-rail supply, ≥10 W (e.g. [MeanWell HDR-15-24](https://www.meanwell.com/Upload/PDF/HDR-15/HDR-15-SPEC.PDF)) | 1 | Powers the OPTA and Wireless for OPTA expansion. Most machine panels already have a 24 VDC rail — source locally only if needed. |
-| Cat6 patch cable, ~2 m | 1 | Point-to-point Modbus TCP link from the OPTA's Ethernet port to the CNC controller's Modbus TCP port. Straight-through; no crossover needed on modern NICs. |
-| DIN rail, ~15 cm | 1 | Mount for the OPTA + expansion. Likely already present inside the panel. |
-
-The Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-
-
-**Safety.** Machine electrical panels contain hazardous voltages on the main power bus, even when control wiring is low-voltage. Installation must be performed by qualified personnel following site lockout/tagout procedures and applicable electrical codes. This reference design is **read-only** over Modbus; it does not issue any commands to the CNC controller.
-
-
-
-1. **Mount and power.** Snap the Wireless for OPTA onto the OPTA's right-hand expansion port. The AUX connector between the two carries the I²C bus that the Notecard rides on; use the solderless AUX connector included with Wireless for OPTA. Mount the assembly on the DIN rail. Wire 24 VDC from the panel supply to the OPTA's `+` and `-` terminals. Per the [Wireless for OPTA quickstart](https://dev.blues.io/quickstart/wireless-for-opta-quickstart/), 24 VDC is required for field deployment. USB-C powers the host CPU for programming but does not power the OPTA's output stage or the expansion. Jumper the OPTA's `+24V` terminal to the expansion's corresponding power input so both share the same supply rail.
-
-2. **Antennas.** Drill or gland two SMA bulkhead connectors through the panel door or a side knockout. Route the primary cellular antenna lead to the first SMA port on Wireless for OPTA and the diversity lead to the second. The bundled rubber-duck antennas are suitable for bench testing only; do not rely on them inside a metal panel in the field.
-
-3. **Modbus TCP link.** Run a Cat6 patch cable from the OPTA's RJ45 Ethernet port to the CNC controller's **dedicated Modbus TCP port** (consult the controller's Modbus/Ethernet option documentation for the correct physical connector and TCP port number). This creates a private point-to-point link: two devices on an isolated `/24` subnet with no routing to the shop floor network. Configure the OPTA with a static IP on the same subnet as the CNC's Modbus TCP interface. Defaults in the firmware: OPTA `192.168.250.10`, CNC `192.168.250.1`. IP addressing is **compile-time only** in the current firmware: adjust `LOCAL_IP` in `cnc_spindle_tracker.ino` (line 40) and `_DEFAULT_CNC_IP` in `cnc_spindle_tracker_helpers.cpp` (line ~30) to match the actual CNC network configuration before building. There is no runtime IP configuration mechanism in this reference design (see [Limitations](#12-limitations-and-next-steps) for the production path).
-
- > **Deployment variant: shared CNC main port.** Some controllers expose Modbus TCP only on the same port used for the machine tool LAN. Connecting the OPTA in that scenario requires joining the customer's machine network (even if only to reach the CNC) and **does not preserve the private-subnet / no-plant-network-touch property** described in §1. That configuration requires customer network and OT-security approval before deployment and is outside the primary scope of this design.
-
-4. **Bench validation setup.** During first-light commissioning, splice the Mojo inline between the 24 VDC supply and the Wireless for OPTA power terminal. This measures the entire expansion subsystem (Notecard plus the expansion's onboard regulators) during a real cellular session.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a new project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino` (line 18, uncomment the `#define PRODUCT_UID` and insert your UID).
-
-2. **Claim the device.** Flash the OPTA and power the panel. On first cellular session the Notecard associates with your project automatically. Check Notehub **Fleet > Devices** within 5–10 minutes to confirm the device appears.
-
-3. **Create a fleet per controller family or model profile.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) let you push a common configuration to every machine that shares a controller type. Fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/), set in the Notehub UI under **Fleet > Environment**, encode the shared parameters: Modbus register block base address, unit ID, port, and alert thresholds. All machines with the same controller model and register map share one configuration without a separate firmware build. Note that IP addressing (`LOCAL_IP` and `DEFAULT_CNC_IP`) remains compile-time in this reference design, so machines whose CNC controllers sit on different network configurations still require a per-build adjustment (see [Limitations](#12-limitations-and-next-steps)). Because register maps frequently differ across CNC models even within the same customer site, organizing fleets by controller family or model profile, rather than solely by site, gives the most precise control over shared settings. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can automate fleet assignment based on a device tag set during commissioning.
-
-4. **Set environment variables.** In the Notehub web UI, navigate to **Fleet > Environment** to view all available variables. All variables are optional; firmware defaults are shown. When you save a change in Notehub the updated value is downloaded to the Notecard on the next inbound sync; the host applies it on the next scheduled `env.get` call, either at boot or at the end of the current report window.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_minutes` | `1` | Minutes between Modbus TCP polls. |
- | `report_minutes` | `60` | Minutes between summary Notes (`cnc_summary.qo`). Also realigns the Notecard outbound sync cadence — both the local summary window and the cellular session schedule update together. |
- | `modbus_port` | `502` | Modbus TCP port on the CNC controller. |
- | `modbus_unit_id` | `1` | Modbus unit (slave) ID. Many CNC controllers default to 1; verify in the CNC controller's Modbus/TCP settings. |
- | `reg_spindle_load` | `256` | Starting address (0-based, wire-level) of the **contiguous six-register block** the firmware reads in a single transaction. The six registers are always read consecutively from this address: spindle load, feed-rate override, alarm code, cycle state, cycle count, operator ID. Set this to the base address where your CNC controller's block begins. |
- | `spindle_overload_pct` | `90.0` | Spindle load (%) above which a `spindle_overload` alarm fires while the machine is in-cycle. |
- | `expected_cycle_sec` | `120` | Reserved for future use. Stored in device config but not transmitted in any Note or evaluated in firmware. |
-
- > **Cadence constraint.** `sample_minutes` must not exceed `report_minutes`. For accurate run/idle minute totals, `report_minutes` should be a whole-number multiple of `sample_minutes` (e.g., `sample_minutes=5, report_minutes=60`). If divisibility is violated (e.g., `sample_minutes=7, report_minutes=60`), run+idle totals in the summary will be less than the window duration. The firmware enforces the first constraint by clamping `sample_minutes` down to `report_minutes` if the env var would otherwise violate it; non-divisible pairs are accepted with a Serial warning. See [§12 Limitations](#12-limitations-and-next-steps).
-
- > **CNC register-map gotchas.** The defaults above are illustrative. Real CNC controllers vary widely on: addressing convention (0-based wire-level vs. 1-based / Fanuc "PLC address" notation); per-register scaling (spindle load may be 0.1 %, 1 %, or % of rated torque); signedness; and whether the cycle count is a 16-bit or 32-bit (two-register) value. Critically, the current firmware only supports a **contiguous six-register layout**: the six values must appear consecutively in the controller's Modbus map starting at `reg_spindle_load`. Production deployments need a vendor-specific register map with a matching contiguous block (or individual per-register reads added to the firmware). See [Limitations](#12-limitations-and-next-steps).
-
-5. **Configure routes.** Add [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) to push data to your downstream systems:
- - **`cnc_alarm.qo`** → real-time delivery to your CMMS, on-call paging system, or Slack webhook.
- - **`cnc_summary.qo`** → batched hourly delivery to your OEE analytics or data warehouse.
- - **`cnc_operator.qo`** → real-time delivery to operator-session or access-logging system (best-effort; individual transitions may be lost to comms outages, and transitions that occur and revert between consecutive polls are never recorded).
-
- Keeping the three Notefiles separate means each can route to a different destination at a different priority without downstream filter logic.
-
-## 7. Firmware Design
-
-The firmware lives in the `firmware/` directory and is split into three files (orchestration, shared types, and the Notecard- and Modbus-facing helpers) so each concern has a clear home:
-
-| File | Role |
-|---|---|
-| [`cnc_spindle_tracker.ino`](firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino) | Main sketch: `setup()`, `loop()`, sample accumulation, alert evaluation, and all global state definitions. |
-| [`cnc_spindle_tracker_helpers.h`](firmware/cnc_spindle_tracker/cnc_spindle_tracker_helpers.h) | Shared types (`Config`, `Sample`, `WindowStats`), compile-time defaults, `extern` declarations for globals, and helper-function prototypes. |
-| [`cnc_spindle_tracker_helpers.cpp`](firmware/cnc_spindle_tracker/cnc_spindle_tracker_helpers.cpp) | Notecard and Modbus helper implementations: `notecardConfigure()`, `defineTemplates()`, `fetchEnvOverrides()`, `modbusConnect()`, `pollCnc()`, `sendSummary()`, `sendAlarm()`, `sendOperatorChange()`, `resetWindow()`. |
-
-### Modules
-
-| Responsibility | Function(s) |
-|---|---|
-| Notecard init, `hub.set` | `notecardConfigure()` |
-| Note template registration | `defineTemplates()` |
-| Environment variable fetch (incremental, time-gated) | `fetchEnvOverrides()` |
-| Ethernet/Modbus TCP connection management | `modbusConnect()` |
-| Modbus register poll (6 registers, 1 transaction) | `pollCnc()` |
-| Rolling hourly stats accumulation | `accumulateSample()` |
-| Alert rule evaluation | `evaluateAlerts()` |
-| Hourly summary Note emission | `sendSummary()` |
-| Immediate alarm Note emission | `sendAlarm()` |
-| Immediate operator-change Note emission | `sendOperatorChange()` |
-| Periodic scheduling (millis-based; no MCU sleep) | `loop()` |
-
-### Sensor reading strategy
-
-Six holding registers are read in a single Modbus TCP transaction (`requestFrom` with quantity 6 starting at `reg_spindle_load`). Batching all six into one round trip is significantly more efficient than six individual reads (one TCP round trip versus six) and keeps the polling window short relative to the one-minute sample interval. The firmware **requires** the six registers to be **contiguous** in the CNC controller's holding-register map; non-contiguous layouts are not supported in this reference design (see [Limitations](#12-limitations-and-next-steps)).
-
-Samples taken while the machine is **idle** (cycle state ≠ 1) are excluded from spindle load and feed-rate averages, because a 0 % spindle reading while the operator is setting up a part isn't useful in the same bucket as a loaded cut. Run and idle minutes are tracked separately so the OEE dashboard can compute an Availability component directly from the summary Note.
-
-### Event payload design
-
-Three [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates store Notes as fixed-length binary records on the Notecard rather than free-form JSON, cutting wire size by 3–5×. This is meaningful for a device that may run for a decade against its included 500 MB.
-
-`cnc_summary.qo` (queued hourly, templated):
-
-```json
-{
- "file": "cnc_summary.qo",
- "body": {
- "spindle_pct_mean": 71.4,
- "spindle_pct_peak": 88.2,
- "feed_override_pct_mean": 97.5,
- "run_min": 51,
- "idle_min": 9,
- "cycle_count": 25,
- "avg_cycle_sec": 122,
- "operator_id": 7,
- "alarm_count": 0,
- "valid_samples": 60
- }
-}
-```
-
-`cycle_count` is the **per-window delta** of the controller's own cumulative cycle-count holding register: the unsigned difference between the register values observed at the start and end of the reporting window. Because it derives from the controller's own accumulator, it captures short cycles that complete entirely within a single poll interval, which edge detection alone cannot see. Counter wrap (65535 → 0) is handled with unsigned 16-bit arithmetic; a controller reset that drops the counter by more than 32767 cannot be distinguished from a natural wrap and would inflate one window's delta.
-
-`avg_cycle_sec` is an edge-timing heuristic derived from `cycleState` register transitions (1 → non-1) and the host MCU's `millis()` clock. It is **not** a controller-native cycle timer. See [§12 Limitations](#12-limitations-and-next-steps) for its known gaps, particularly on fast-cycle jobs.
-
-`valid_samples` is the count of successful Modbus reads in the window. A value of `0` means every poll failed this window; downstream analytics use this sentinel to distinguish a total communication outage from a machine that was genuinely powered and idle.
-
-`cnc_alarm.qo` (immediate, `sync:true`, templated):
-
-```json
-{
- "file": "cnc_alarm.qo",
- "sync": true,
- "body": {
- "alert_type": "spindle_overload",
- "alarm_code": 0,
- "spindle_pct": 93.1,
- "operator_id": 7
- }
-}
-```
-
-`alert_type` is one of `spindle_overload` (spindle load exceeded threshold during a cut), `cnc_alarm` (CNC control raised a fault code), or `modbus_unreachable` (Modbus TCP connection lost, rate-limited to once per summary window).
-
-`cnc_operator.qo` (immediate, `sync:true`, templated):
-
-```json
-{
- "file": "cnc_operator.qo",
- "sync": true,
- "body": {
- "operator_id": 9,
- "prev_operator_id": 7
- }
-}
-```
-
-Fires whenever the value in the controller's operator-ID holding register changes between consecutive samples. `operator_id` is the new (incoming) value; `prev_operator_id` is the outgoing value. `operator_id == 0` conventionally indicates no operator is logged in. Events are **best-effort** and sampled at the poll interval: a transient comms outage can drop an individual transition Note, and any operator-ID change that occurs and reverts between two consecutive polls is invisible to the firmware. The hourly `cnc_summary.qo` captures the most recently observed operator ID at window close: a state snapshot, not a complete record of all transitions that occurred during the window.
-
-### Power and sync strategy
-
-The OPTA + Wireless for OPTA is 24 VDC line-powered, so host MCU sleep is not the design goal; bandwidth and bus efficiency are. The Notecard runs in [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `periodic` mode with `outbound` and `inbound` cadences initialized at startup to `DEFAULT_REPORT_MINUTES` (60 minutes) and `DEFAULT_REPORT_MINUTES × 2` (120 minutes) respectively. When `report_minutes` changes via an env-var update, the firmware reissues `hub.set` with the new `outbound` and `inbound` values so the Notecard's cellular session schedule stays aligned with the summary window. Summary Notes queue on-device and ship in a single session per window; alarm Notes carry `sync:true` and wake the radio within a session-establishment window (typically 15–60 seconds). The firmware does not artificially spin the Modbus bus faster than the sample interval: one poll per `sample_minutes`, then the host simply yields in `loop()` until the next scheduled tick.
-
-### Retry and error handling
-
-- `notecardConfigure()` uses `notecard.sendRequestWithRetry(req, 5)` inside a blocking loop that repeats every 30 seconds until `hub.set` succeeds. Notecard configuration is treated as mandatory: without a valid `hub.set` the ProductUID is not registered on the Notecard, the outbound/inbound cadence is wrong, and subsequent Notes would be un-routable. The device does not enter the main loop until configuration is confirmed.
-- Modbus TCP connection loss is detected on the `requestFrom()` return code. The firmware retries once per sample (reconnect + re-poll). If both fail, the sample is skipped and a `modbus_unreachable` alarm fires, rate-limited to once per summary window to avoid alarm fatigue when the CNC is simply powered off.
-- `notecard.requestAndResponse()` responses are checked for both `NULL` return and the `err` field before any field is read from the response.
-- Environment variable fetches use the `time` argument to request only variables modified since the last successful fetch; no unnecessary data is transferred on inbound syncs.
-- Alert de-duplication: `spindle_overload` carries a 30-minute cooldown timer so a slow-climbing load doesn't page the on-call every sample. CNC alarm codes fire on any transition to a nonzero value (0 → nonzero, or one nonzero code changing to a different nonzero code), not on every sample where a fault is asserted. Alarm-state tracking (`g_lastAlarmCode`, `g_window.alarmCount`) is updated immediately when the transition is **observed**, independent of whether the `cnc_alarm` Note queues successfully. If the Note fails, the alarm is pushed into an in-memory ring buffer (`g_alarmFifo`, depth 8) and retried one slot per poll cycle, ensuring a comm outage cannot cause the summary to undercount alarm transitions or silently lose a short-lived alarm. On ring-buffer overflow the oldest slot is evicted and a warning is logged to Serial.
-
-### Key code snippet 1: Notecard periodic sync configuration
-
-```cpp
-// Block until hub.set succeeds — Notecard configuration is mandatory.
-// Without a valid hub.set the ProductUID is not registered and Notes are
-// un-routable; there is no useful operating state without it.
-while (true) {
- J *req = notecard.newRequest("hub.set");
- if (req == NULL) { delay(5000); continue; }
- JAddStringToObject(req, "product", productUid);
- JAddStringToObject(req, "mode", "periodic");
- JAddNumberToObject(req, "outbound", DEFAULT_REPORT_MINUTES); // hourly sync
- JAddNumberToObject(req, "inbound", DEFAULT_REPORT_MINUTES * 2);
- // sendRequestWithRetry handles the cold-boot I²C race where the host
- // MCU comes up before the Notecard is ready.
- if (notecard.sendRequestWithRetry(req, 5)) break;
- usbSerial.println("[NOTECARD] hub.set failed — retrying in 30 s.");
- delay(30000);
-}
-```
-
-### Key code snippet 2: Modbus TCP batch read (6 registers, 1 transaction)
-
-```cpp
-// Read all 6 contiguous holding registers in a single Modbus TCP round-trip.
-// Reading in bulk is ~6× faster than individual requests and keeps the
-// 60-second sample window short enough to be unnoticeable by the operator.
-if (!modbusTCPClient.requestFrom(cfg.modbusUnitId, HOLDING_REGISTERS, regBase, 6)) {
- return false; // caller handles retry
-}
-int16_t rawSpindle = (int16_t)modbusTCPClient.read(); // 0.1 % units
-// Register N+1 is feed-rate override (0–150 % of programmed rate), not
-// actual feed rate in mm/min — see §11 Limitations.
-int16_t rawFeedOverride = (int16_t)modbusTCPClient.read();
-s.alarmCode = (uint16_t)modbusTCPClient.read();
-s.cycleState = (uint8_t)(modbusTCPClient.read() & 0xFF);
-s.cycleCount = (uint16_t)modbusTCPClient.read();
-s.operatorId = (uint16_t)modbusTCPClient.read();
-s.spindleLoadPct = rawSpindle / 10.0f;
-s.feedOverridePct = rawFeedOverride / 10.0f; // e.g. 1000 → 100.0 % of programmed rate
-```
-
-### Key code snippet 3: Immediate alarm Note with sync:true
-
-```cpp
-// sync:true bypasses the hourly outbound window — the Notecard wakes the
-// radio and delivers the alarm within the session-establishment window.
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "cnc_alarm.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert_type", alertType);
-JAddNumberToObject(body, "alarm_code", (int)s.alarmCode);
-JAddNumberToObject(body, "spindle_pct", s.spindleLoadPct);
-JAddNumberToObject(body, "operator_id", (int)s.operatorId);
-notecard.sendRequest(req);
-```
-
-## 8. Build and Flash
-
-**Prerequisites:**
-- Arduino IDE 2.3+ or `arduino-cli` (command-line). If using the IDE, install the **Arduino Mbed OS Opta Boards** board package via Tools > Board Manager — search for "Opta".
-- **Dependencies** (install via Library Manager or `arduino-cli`):
- - [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (check [releases](https://github.com/blues/note-arduino/releases))
- - [`ArduinoModbus`](https://github.com/arduino-libraries/ArduinoModbus)
- - [`ArduinoRS485`](https://github.com/arduino-libraries/ArduinoRS485)
- - `Ethernet.h` (included with Mbed OS Opta Boards core)
-
-**Build steps (using `arduino-cli`):**
-
-```bash
-# Install the board core
-arduino-cli core install arduino:mbed_opta
-
-# Install dependencies
-arduino-cli lib install "Blues Wireless Notecard" "ArduinoModbus" "ArduinoRS485"
-
-# Edit firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino — uncomment line 18 and paste your ProductUID
-# Edit firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino line 40 — set LOCAL_IP to match your Modbus subnet
-# Edit firmware/cnc_spindle_tracker/cnc_spindle_tracker_helpers.cpp line ~30 — set _DEFAULT_CNC_IP to your CNC controller IP
-
-# Compile
-arduino-cli compile -b arduino:mbed_opta:opta firmware/
-
-# Flash (replace /dev/cu.usbmodem1234567 with your OPTA's USB port)
-arduino-cli upload -b arduino:mbed_opta:opta -p /dev/cu.usbmodem1234567 firmware/
-
-# Open the serial monitor to check for errors
-arduino-cli monitor -p /dev/cu.usbmodem1234567 -c baudrate=115200
-```
-
-**Alternatively, using the Arduino IDE:** Open `firmware/cnc_spindle_tracker/cnc_spindle_tracker.ino`, configure your ProductUID and IPs, select Tools > Board > Arduino OPTA, and click Upload.
-
-## 9. Data Flow
-
-
-
-**Collected.** Every `sample_minutes` (default 1 minutes): spindle load (%), feed-rate override (%), active alarm code, cycle state, cumulative cycle count, and current operator ID (the value presently in the controller's operator-ID register): six registers in one Modbus TCP transaction.
-
-**Accumulated.** Each sample updates rolling counters: spindle load sum and peak (while running), feed-rate sum (while running), run/idle minute tallies, the per-window cycle-count register delta (difference between consecutive cycleCount register reads, primary source for `cycle_count`), edge-timed cycle durations (for `avg_cycle_sec` only), observed active-alarm-transition count (alarms that assert and clear between polls are never seen), and operator-ID change detection. Cycles that complete entirely within one poll interval are invisible to the edge detector but are still captured in the register delta. See [Limitations](#12-limitations-and-next-steps) for the practical implications of the `avg_cycle_sec` heuristic.
-
-**Transmitted.**
-- `cnc_summary.qo`: once per `report_minutes` (default: 24 Notes per day per machine), queued locally and shipped in the Notecard's hourly outbound sync. Fields: spindle mean/peak, feed-rate override mean (`feed_override_pct_mean`, override percentage of the programmed rate, not engineering-unit feed rate; see §11), run minutes, idle minutes, `cycle_count` (per-window delta of the controller's cumulative cycle-count register), `avg_cycle_sec` (edge-timing heuristic; see Limitations), most-recently-observed operator ID, `alarm_count` (count of *observed* active-alarm transitions during the window, not a complete fault history; see [§12 Limitations](#12-limitations-and-next-steps)), `valid_samples` (0 signals a total Modbus communication outage for the window).
-- `cnc_alarm.qo`: immediately on alert trigger, `sync:true`. Three alert types: `spindle_overload`, `cnc_alarm`, `modbus_unreachable`.
-- `cnc_operator.qo`: immediately on operator-ID register transition, `sync:true`. Fields: `operator_id` (incoming value), `prev_operator_id` (outgoing value). Best-effort and sampled: individual events may be lost to comms outages, and any transition that occurs and reverts between consecutive polls is never recorded. The hourly summary captures the last observed ID at window close, not a complete transition history.
-
-**Routed.** Notehub routes `cnc_alarm.qo` in real time to the OEM's CMMS or on-call system; `cnc_summary.qo` goes to the OEE analytics store at hourly cadence; `cnc_operator.qo` routes in real time to an access-logging or per-operator OEE attribution system.
-
-**Alert triggers.**
-- `spindle_overload`: spindle load exceeds `spindle_overload_pct` while the machine is in-cycle (state = 1). 30-minute cooldown prevents repeat paging on a sustained overload condition.
-- `cnc_alarm`: active alarm code transitions to any nonzero value (0 → nonzero, or one nonzero code changing to a different nonzero code) as **observed** on the poll; fires immediately on the observed transition. Alarms that assert and clear between consecutive polls are never seen and produce no Note.
-- `modbus_unreachable`: Modbus TCP connection fails on two consecutive attempts; rate-limited to once per `report_minutes` window.
-
-## 10. Validation and Testing
-
-**Expected steady-state behavior.** A healthy machine generates one `cnc_summary.qo` per hour and zero `cnc_alarm.qo` Notes. On first commissioning, you may see one `modbus_unreachable` alarm until the Modbus TCP link is confirmed.
-
-**Check Notehub for events.** Sign in to [notehub.io](https://notehub.io), navigate to **Fleet > Events**, and filter for your device. Within 1 hour of power-up, you should see a `cnc_summary.qo` Note with fields like `spindle_pct_mean`, `run_min`, `cycle_count`, etc. Check the **body** tab to inspect the actual JSON.
-
-**Modbus first-light (bench test).** Before connecting to the real CNC:
-1. Run a Modbus TCP simulator (Modbus Mechanic, ModRSsim2, ModbusMaster, or any open-source server) on your laptop.
-2. Connect it to the OPTA Ethernet port (same subnet as OPTA IP, e.g., both on `192.168.250.0/24`).
-3. Create six contiguous holding registers at address 256 with demo values: spindle load 650 (6.5%), cycle state 1 (running), alarm code 0, cycle count 10, operator ID 1.
-4. Flash the firmware pointing to your demo server (set `_DEFAULT_CNC_IP` to the simulator's address).
-5. Wait one sample period (1 minutes default) plus one report window (60 minutes default) for the first summary to appear in Notehub. Validate that `spindle_pct_mean ≈ 6.5`, `cycle_count = 0` (register delta), `valid_samples = 60`.
-6. Increase the spindle-load register to 950 (95%) and wait 1 minute. Within 60 seconds of that poll, a `cnc_alarm.qo` with `alert_type: "spindle_overload"` should arrive in Notehub.
-
-**Field alert testing.** To test alert delivery on a live machine:
-1. In Notehub, navigate to **Fleet > Environment**.
-2. Temporarily set `spindle_overload_pct = 0`.
-3. Wait for the next Notecard inbound sync (default every 120 minutes), then the host will apply the new threshold on the next report window boundary.
-4. Any nonzero spindle reading in the next poll will trigger a `cnc_alarm.qo`.
-5. Reset `spindle_overload_pct` to its normal value (e.g., 90) once tested.
-
-**Power validation (optional Mojo).** Splice the [Mojo coulomb counter](https://dev.blues.io/datasheets/mojo-datasheet/) between the 24 VDC supply and the Wireless for OPTA power input. Expected current envelope (from [Notecard low-power design](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)):
-
-| Phase | Expected current |
-|---|---|
-| Notecard idle (radio off, between syncs) | ~8–18 µA @ 5 V |
-| Cellular session (LTE Cat-1) | ~250 mA average; ≤2 A burst |
-| WiFi fallback session | ~80 mA average |
-
-Confirm: (a) idle current between syncs is in the µA range (ensures the device will run for years on field deployment), (b) per-session energy for one hourly sync is ~10–30 mAh, and (c) energy is consistent across consecutive cycles.
-
-
-
-**Bench caveat:** When the OPTA is powered and programmed over USB-C, the USB rail keeps the Notecard in a higher-power idle state. Do not expect µA idle current while USB is connected. For true field idle measurements, power from 24 VDC only (disconnect USB) and verify the conditions in [Notecard low-power design docs](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/).
-
-
-
-## 11. Troubleshooting
-
-| Symptom | Probable Cause | Solution |
-|---------|----------------|----------|
-| Device does not claim to Notehub after first power-up | Missing or malformed `PRODUCT_UID` in firmware | Uncomment and set `PRODUCT_UID` in `cnc_spindle_tracker.ino` line 18; reflash. |
-| No Modbus connection; continuous `modbus_unreachable` alarms | Wrong CNC IP or port; Ethernet cable unplugged; CNC controller powered off or Modbus not enabled | Confirm CNC controller's Modbus TCP IP and port in its documentation. Verify the IP in `_DEFAULT_CNC_IP` (helpers.cpp) matches the CNC. Check Cat6 cable is plugged in at both ends. Confirm `modbus_port` env var matches the CNC's TCP port (default 502). |
-| No `cnc_summary.qo` Notes appear in Notehub | Device claimed, but no summaries visible | Check the Notecard's outbound sync interval has elapsed (default 60 minutes). Verify `valid_samples > 0` in any alarm Notes — if `valid_samples = 0`, all Modbus polls in that window failed; check network. Use `arduino-cli monitor` to watch Serial output for poll successes/failures. |
-| `valid_samples = 0` in all summaries | All Modbus polls failed | Serial console should show Modbus errors. Verify CNC is powered and Modbus TCP enabled. Use a Modbus client tool (QModBus, ModRSsim2) on your laptop to confirm you can reach the CNC at the configured IP and port. If you can, but the OPTA cannot, there may be a routing or firewall issue on the Ethernet segment. |
-| Notecard not syncing; no Notes leaving the device | I²C communication between OPTA and Notecard failed; or Notecard not powered | Verify the AUX connector is fully seated. Check 24 VDC is applied to both OPTA and Wireless for OPTA. The Notecard has a small blue LED near the SMA connectors — it should blink during a cellular session. If no blink, the Notecard may not be powered or may have failed. |
-| `spindle_overload` alarms fire too frequently | Threshold too low; or noisy spindle-load sensor data | Increase `spindle_overload_pct` in Fleet environment (e.g., from 90 to 95). If the issue persists, the CNC sensor may be noisy; add averaging on the CNC side (most controllers have digital-filter registers) or increase `SPINDLE_ALERT_COOLDOWN_MS` in `cnc_spindle_tracker_helpers.h`. |
-| `avg_cycle_sec` is wildly wrong | Cycle time shorter than sample interval; or `cycleState` semantics differ from expected | `avg_cycle_sec` is a heuristic limited by the sample period (1 minutes default) — cycles faster than that are invisible. Increase `sample_minutes` (e.g., to 0.25 for 15-second samples) if your cycles are fast, but understand this increases Modbus polling overhead. Verify the target CNC model maps `cycleState == 1` to "program running" (vendor-specific). |
-| Operator-ID transitions not appearing | `operator_id` register not exposed or always zero | Confirm the CNC controller supports operator-ID over Modbus TCP (many do not). Verify the sixth register in the contiguous block (starting at `reg_spindle_load`) is mapped to operator ID in the controller's Modbus documentation. Check the operator ID is actually changing on the machine (some controllers require login/logout). |
-
-## 12. Limitations and Next Steps
-
-This reference design is deliberately scoped to the moment the OEM's service technician needs most: from shipping crate to first usable telemetry on a single CNC, in an afternoon, without a single conversation with the shop's IT department. A few details were left simple to keep that path clean; each is documented below, along with what the production hardening looks like.
-
-### Simplified for this reference design
-
-Each of the simplifications below is a deliberate scope choice: a place where a production deployment will validate a vendor register map, add configurability, or harden a comms path once the basic single-CNC monitor is proven.
-
-**Active alarm register only; transient alarms between polls are invisible.** The firmware reads the CNC controller's *currently active* alarm-code holding register once per `sample_minutes` poll. It does **not** read a fault-history log, alarm queue, or event recorder. Most controllers maintain such a log internally, but standard Modbus TCP interfaces rarely expose it. Any alarm that asserts and clears entirely within one poll interval is never observed, produces no `cnc_alarm.qo` Note, and is not counted in `alarm_count` in the hourly summary. The `alarm_count` field therefore reflects the number of *observed* active-alarm transitions during the window, not a complete record of every fault the controller encountered. This is not equivalent to reading the controller's internal fault history. For comprehensive fault logging, access the controller's native alarm log directly: via the operator panel, vendor software, or (where supported) a proprietary interface such as Fanuc FOCAS or Siemens OPC-UA. The practical consequence: increase `sample_minutes` and the probability of missing a brief alarm rises proportionally. Keep the poll interval at 1 minute to minimize (but not eliminate) the gap.
-
-**Feed-rate override as a proxy for feed rate.** The firmware reads register N+1 of the six-register block and interprets it as **feed-rate override percentage**: the operator-set multiplier applied to the programmed feed rate, typically 0–150 %. This is not the same as actual feed rate in engineering units (mm/min or in/min). Engineering-unit feed rate is rarely exposed over Modbus TCP on typical CNC controllers: most vendors reserve that value for internal NC interpolation and either do not map it to a Modbus register or require a proprietary protocol to access it (e.g., Fanuc FOCAS, Siemens OPC-UA). Feed-rate override is a useful production proxy: it surfaces the operator behavior described in §1 (over-riding the programmed rate on finishing passes) and is sufficient for the EaaS monitoring use case, but it is not a substitute for engineering-unit feed-rate telemetry in a rigorous Performance calculation. The `feed_override_pct_mean` field in `cnc_summary.qo` is named accordingly; downstream analytics should document this distinction.
-
-**`sample_minutes` must not exceed `report_minutes`, and ideally must divide it evenly.** `run_min` and `idle_min` in `cnc_summary.qo` are computed by adding `sample_minutes` to the appropriate accumulator on every successful poll. Two invariants must hold for those totals to be valid. First, `sample_minutes` ≤ `report_minutes`; otherwise a single poll adds more minutes than the entire report window, producing run/idle counts that exceed the window duration. Second, `report_minutes` should be a whole-number multiple of `sample_minutes` (e.g., `sample_minutes=5, report_minutes=60`); otherwise the run+idle total will be less than the window duration even when the machine is continuously active, because the accumulator increments in fixed steps that do not land exactly on the report boundary. The firmware enforces the first invariant by clamping `sample_minutes` down to `report_minutes` in `fetchEnvOverrides()` and logging a warning to Serial. The second invariant is advisory: non-divisible combinations are accepted with a Serial warning but produce approximate run/idle totals. Configure fleet environment variables with divisible pairs.
-
-**Demo register map only.** The firmware reads six contiguous 16-bit holding registers starting at address 256, with fixed 0.1-unit scaling. Real CNC controllers differ on: addressing convention (0-based wire-level vs. Fanuc PLC notation vs. Siemens DBx addressing); per-register scaling; signedness; 32-bit cycle counters spanning two registers with vendor-specific word order; and which registers are even exposed over Modbus vs. proprietary protocol (Fanuc FOCAS, Siemens OPC-UA, Haas NGC). Each vendor requires a validated register map before this design can be deployed to production machines.
-
-**IP addressing is hardcoded.** The CNC server IP and OPTA local IP are compile-time `#define`s. A production deployment needs either a web-based local configuration UI, an in-panel DIP-switch scheme, or env-var parsing of dotted-decimal IP strings, none of which are implemented here.
-
-**Operator-change events are best-effort, not guaranteed.** The firmware detects operator-ID transitions by comparing consecutive register reads and immediately emits a `cnc_operator.qo` Note on each change. Because `sendOperatorChange()` does not retry on failure, a transient I²C or cellular comms outage can drop an individual transition Note. The hourly `cnc_summary.qo` carries the most recently observed operator ID as a window-close snapshot; it does not capture all transitions that occurred during the window, and any transition that occurs and reverts between polls is never recorded. Per-operator session durations and cumulative utilization derived from summary Notes are therefore approximate and cannot substitute for durable session accounting. As with all register-based reads, the `operator_id` field is only as reliable as the controller's implementation: Fanuc 0i-MF exposes operator ID in the PMC area; Siemens SINUMERIK exposes it via OPC-UA rather than Modbus; some controllers have no external operator-identity interface at all. Where unavailable, `operator_id` will be 0 on every sample.
-
-**`cycle_count` is authoritative; `avg_cycle_sec` is a heuristic.** `cycle_count` in `cnc_summary.qo` is the per-window **delta** of the controller's own cumulative cycle-count holding register, computed with unsigned 16-bit subtraction of consecutive register reads. It captures every cycle that increments the controller's register, including cycles that complete entirely within a single poll interval, which edge detection alone cannot see. Counter wrap (65535 → 0) is handled correctly; a mid-session controller reset that drops the counter by more than 32767 cannot be distinguished from a natural wrap and would inflate one window's delta (an accepted corner case given the rarity of such resets).
-
- `avg_cycle_sec`, by contrast, is an edge-timing heuristic: the firmware watches `cycleState` transitions (1 → non-1) and measures elapsed wall-clock time between them using `millis()`. Two systematic limitations apply. **Missed short cycles:** any cycle that starts and finishes within a single poll interval is invisible to the edge detector; those cycles are excluded from `avg_cycle_sec` even though they are counted in `cycle_count`. On fast-cycle jobs (cycle time shorter than `sample_minutes`), `avg_cycle_sec` is biased toward the longer, observable cycles. **Timing granularity:** accuracy is bounded by the sample period (1 minute by default), not the controller's internal timer. Treat `avg_cycle_sec` as a first-order estimate, not a certified measurement. The mapping of `cycleState == 1` to "program running" is vendor-specific and must be verified against each target CNC model before relying on `avg_cycle_sec`.
-
-**Quality component of OEE is absent.** OEE = Availability × Performance × Quality. This design provides the raw data for Availability (run/idle minutes) and the device-side inputs for Performance (cycle count and average cycle time, transmitted in `cnc_summary.qo`). However, the comparison of average cycle time against an expected target must be configured entirely in the downstream analytics layer today; `expected_cycle_sec` is stored in device config for future use but is not evaluated in firmware nor transmitted in any Note. Quality, the ratio of conforming parts to total parts, requires a downstream measurement (CMM output, vision inspection, or operator scrap entry) that Modbus cannot supply.
-
-**Single CNC per OPTA.** The firmware polls one Modbus slave ID. A multi-machine cell with a shared Modbus gateway could support multiple IDs by round-robin polling, but that complicates the per-machine summary bookkeeping and is out of scope here.
-
-**No Modbus writes.** This is intentional and non-negotiable for this reference design. Writing setpoints to a CNC (feed override, spindle speed, program selection, M-codes) involves machine safety, interlocks, and functional safety certification that are entirely outside the scope of a monitoring-only device.
-
-**Modbus TCP reconnection is simple.** The firmware reconnects on every failed transaction. A production hardening would add exponential backoff, a connection health watchdog, and a per-session keep-alive to detect stale TCP connections before the next poll.
-
-**No host firmware updates wired up.** [Notecard Outboard Firmware Update](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) is supported on STM32H7 (the OPTA's MCU family) but requires AUX wiring that Blues Wireless for OPTA does not currently break out. Host firmware updates are local-only via USB-C for the current Wireless for OPTA hardware.
-
-### Production Next Steps
-
-Taking this monitor toward a production fleet means validating vendor-specific register maps, adding field configurability, and closing the remaining OEE loop. The following extensions are the natural progression, roughly from the most immediately useful toward the most integration-dependent.
-
-**Vendor-specific register-map builds** are what most deployments will need first: Fanuc 30i/31i/32i (Series 30), Siemens SINUMERIK 840D sl (via OPC-UA adapter), Mitsubishi M80/M800, and Haas NGC, each with correct addressing, scaling, and protocol translation where Modbus TCP is not natively available.
-
-**IP addressing via Notehub environment variables** (string dotted-decimal parsing) or a local web configurator served from the OPTA on first-boot removes the compile-time `#define` constraint.
-
-**32-bit cycle counter support** reads two consecutive registers and assembles them with correct vendor byte order.
-
-**Native elapsed-cycle-timer reads** replace the heuristic `cycleState` edge-detection used for `avg_cycle_sec` with a read of the controller's own elapsed-cycle-timer register (where exposed by the vendor). This removes the timing bias introduced by poll-interval granularity and the dependency on vendor-specific `cycleState` semantics. `cycle_count` already uses the controller's cumulative register; this step completes the transition for the average timing field.
-
-**OEE Quality component integration** via an inbound Notefile (`cnc_quality.qi`) that receives scrap/conforming counts from a CMM or vision system closes the OEE loop on the device.
-
-**Per-machine baseline learning** accumulates a 30-day spindle-load-vs-feed-rate operating envelope and triggers `spindle_overload` against the machine's own learned curve rather than a static percentage.
-
-**Wiring ODFU to the OPTA's BOOT/RESET pins** enables over-the-air host updates once the AUX path is available.
-
-## 13. Summary
-
-The OEM whose six-figure machining centers have been silent ever since the shop floor accepted delivery now sees the picture they never had: spindle load, cycle counts, alarm codes, and operator IDs flowing back hourly, with overload and fault transitions arriving in real time, all over a cellular channel that machine-shop IT never sees and never needs to approve. CNC controllers that expose telemetry over Modbus TCP have always had this data on board; the OT-network policies that protect the plant are what kept it stranded. A direct point-to-point Ethernet cable from the OPTA to the machine control captures the registers locally, and the Notecard inside Blues Wireless for OPTA carries them to the OEM's cloud on its own independent uplink. The result is the raw material for a credible equipment-as-a-service offer: utilization data granular enough to bill by the spindle-hour, alarm telemetry detailed enough to offer a proactive-service contract, and OEE components accurate enough to benchmark the fleet. What's left is a validated, vendor-specific Modbus TCP register map for each target CNC model: a commissioning task, not an architecture problem.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/cnc-machine-spindle-load-and-cycle-time-tracker/)**
diff --git a/68-cnc-machine-spindle-load-cycle-time-tracker/banner.png b/68-cnc-machine-spindle-load-cycle-time-tracker/banner.png
deleted file mode 100644
index 57d2c616..00000000
Binary files a/68-cnc-machine-spindle-load-cycle-time-tracker/banner.png and /dev/null differ
diff --git a/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/01-system-architecture.svg b/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/01-system-architecture.svg
deleted file mode 100644
index 357c3173..00000000
--- a/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-
diff --git a/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/02-wiring-assembly.svg b/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 123d24cb..00000000
--- a/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,83 +0,0 @@
-
diff --git a/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/03-data-flow.svg b/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/03-data-flow.svg
deleted file mode 100644
index fa5d2b69..00000000
--- a/68-cnc-machine-spindle-load-cycle-time-tracker/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,82 +0,0 @@
-
diff --git a/69-injection-molding-shot-to-shot-process-monitor/README.md b/69-injection-molding-shot-to-shot-process-monitor/README.md
index 4b5ca652..ae267556 100644
--- a/69-injection-molding-shot-to-shot-process-monitor/README.md
+++ b/69-injection-molding-shot-to-shot-process-monitor/README.md
@@ -1,493 +1,5 @@
-# Injection Molding Hydraulic Injection Pressure Monitor
+# Injection Molding Shot-to-Shot Process Monitor
-
+A cellular shot-level process monitor for plastic injection molding machines that captures injection pressure and mold temperature, reducing each shot to key metrics for cloud-based process analysis.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Injection%20Molding%20Hydraulic%20Injection%20Pressure%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/injection_molding_shot_monitor/](firmware/injection_molding_shot_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a shot-level process monitor for plastic [injection molding](https://blues.com/industrial-equipment-monitoring/) machines. The device captures the two signals that drive part quality on every shot (hydraulic injection pressure and mold temperature) and reduces each shot to a handful of summary metrics a process engineer would want to see (peak pressure, fill time, pack pressure, average mold temperature, shot sequence number), then pushes them to the cloud over cellular. Because the connectivity sits outside the plant's OT network, no IT or network ticket is required at the customer site. The hardware is a Blues Notecarrier CX paired with a [Notecard Cell+WiFi](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link).
-
-## Expected Outcome
-
-After completing this project, you will have:
-- A compact DIN-rail enclosure mounted in the machine's electrical cabinet
-- Hydraulic injection pressure and mold temperature collected continuously at the edge
-- Five shot-level metrics (`peak_psi`, `fill_ms`, `pack_psi`, `temp_avg_c`, cooling-rate slope) in the [Blues Notehub](https://blues.com/notehub/) cloud service's `shot.qo` Notefile — one event per shot (or per N shots, configurable)
-- Real-time alerts routed to your CMMS, webhook, or quality system whenever any metric exceeds configurable thresholds
-- No modification to the machine, no touching the mold, no plant-network involvement: 100% cellular
-- Commissioning takes ~50–100 shots to establish baseline thresholds; thereafter the system runs autonomously
-
-## 1. Project Overview
-
-**The problem.** Plastic injection molding is a process that looks stable until it isn't. A mold that cycled flawlessly through ten thousand shots can begin producing scrap on shot ten-thousand-and-one: a worn gate land, a slightly off-spec resin lot, a cooling circuit that's partially fouled, a worn check ring on the screw. None of these failures announce themselves dramatically. What they do is introduce subtle, shot-to-shot variation in the two signals that matter most: hydraulic injection pressure at the cylinder's manifold block (the upstream hydraulic forcing function that drives how completely the cavity fills and how aggressively it is packed) and mold temperature (which governs cooling rate, crystallinity, and cycle time). Both of those signals are measurable continuously. Almost nobody measures them continuously.
-
-**Why Notecard.** Plastics processors run their production equipment on isolated **OT** (operational technology) networks, isolated by design, because the machines that run 24/7 production cannot share a network path with corporate IT systems. An OEM that wants to offer a remote process-monitoring service faces a hard choice: either negotiate a network path through every customer's IT and OT security teams (slow, expensive, different every time), or deploy a connectivity solution that bypasses the plant network entirely. The Notecard Cell+WiFi variant does exactly that: it carries data directly to [Notehub](https://notehub.io) over cellular, with WiFi available as an opportunistic fallback for plants that permit it, and the OEM ships the same hardware SKU to every customer regardless of how the plant's networks are organized.
-
-
-
-**Deployment scenario.** A compact DIN-rail or panel-mount enclosure installed in or adjacent to the machine's electrical cabinet, powered from the machine's existing 24 VDC control rail. The hydraulic injection pressure transducer taps into a 1/4-18 NPT port on the injection cylinder's hydraulic manifold block, on the hydraulic oil side of the machine, with no contact with the polymer melt, so **no mold modification is required**. The thermocouple probe is seated in the mold's existing thermocouple pocket. No modification to the machine's controller, no connection to the plant LAN, no IT ticket.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Because the machine itself is line-powered, the Notecarrier CX's onboard Cygnet STM32L433 host stays running; there's no deep-sleep cycle to manage. Between shots it polls injection-manifold pressure at 10 Hz, watching for the rising edge that means the screw has started its push forward. The moment that signal crosses `shot_detect_psi` the host flips into 20 Hz capture mode (50 ms per sample), filling a RAM buffer with the pressure and temperature profile for the duration of the shot. When the shot completes, the host pulls five shot-level features out of the buffer, increments the per-boot-session shot counter, and queues a [Note](https://dev.blues.io/api-reference/glossary/#note) for the Notecard to handle. All of it fits in the STM32's 64 KB SRAM: a 102-second shot at 20 Hz is at most 16 KB of profile data, well inside the available budget.
-
-**Notecard responsibilities.** Each Note the host hands off goes into the Notecard's on-device queue. From there the Notecard manages everything radio-side — it brings up the cellular (or WiFi) session on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes) for routine `shot.qo` Notes and flushes anything marked `sync:true` immediately, regardless of the outbound schedule. The same channel runs in the other direction for [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/): a process engineer can retune detection thresholds and alert bands from Notehub without ever reflashing the device.
-
-**Notehub responsibilities.** The Notecard manages its own cellular session against the supported carrier networks worldwide via its embedded global SIM, then delivers data to [Notehub](https://dev.blues.io/notehub/notehub-walkthrough/) over the Internet. Notehub ingests every event, stores it, and applies project-level [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). The split into two [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) (`shot.qo` for the per-shot process data and `shot_alert.qo` for out-of-spec events) is what lets each stream go to a different downstream destination at a different urgency, no filter logic required.
-
-**Routing (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations. This project ships no specific downstream endpoint. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for setup options.
-
-## 3. Technical Summary
-
-**Path to First Event (5–10 min bench demo)**
-
-1. **Assemble the minimum bench kit** (no production hydraulics needed):
- - Notecarrier CX + Notecard Cell+WiFi
- - SparkFun MAX31855K thermocouple breakout + dummy K-type leads
- - 150 Ω resistor, 100 nF capacitor, 5 V USB power or MeanWell DC-DC
- - Jumper wires to connect A0 (pressure via resistor) and SPI pins (thermocouple)
-
-2. **Create a Notehub project and get the ProductUID:**
- - Sign up at [notehub.io](https://notehub.io)
- - Create a new project and copy its ProductUID (format: `com.company:project-name`)
-
-3. **Flash the firmware:**
- ```bash
- arduino-cli core install "STMicroelectronics:stm32"
- arduino-cli lib install "Blues Wireless Notecard"
- # Edit firmware/injection_molding_shot_monitor/injection_molding_shot_monitor.ino
- # Replace PRODUCT_UID "" with your actual ProductUID
- arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/injection_molding_shot_monitor/
- arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/ttyACM0 firmware/injection_molding_shot_monitor/
- ```
-
-4. **Monitor the serial output:**
- - Open a serial terminal at 9600 baud to see shot detection and feature extraction logs
- - Inject a fake pressure pulse (e.g. short the 150 Ω resistor for ~2 seconds) to trigger a shot
- - Verify the next Notehub sync (within 60 min) shows a `shot.qo` event
-
-5. **Inspect the event in Notehub:**
- - Log into notehub.io, open your project, and navigate to Events
- - Look for `shot.qo` events containing `cycle`, `peak_psi`, `fill_ms`, `temp_avg_c`
- - The serial log and Notehub events are the source of truth during commissioning
-
-
-
-**Design scope: hydraulic injection pressure, no mold modification required.** This monitor measures **hydraulic injection pressure at the injection cylinder's manifold block** using an off-the-shelf 4–20 mA strain-gauge transducer that taps a standard NPT port on the hydraulic circuit. Hydraulic pressure is the forcing function the injection unit applies; in-cavity pressure is the actual polymer pressure the mold cavity sees. The two differ by nozzle, gate, and runner losses that vary with resin, temperature, and wear. Because the measurement point is on the hydraulic circuit rather than inside the mold, this approach can be retrofitted to any hydraulic press in an afternoon without touching the mold, without accessing a sensor port, and without production interruption. It is appropriate for shot-to-shot consistency monitoring, filling and packing trend detection, and early-warning alerting. It is **not** a substitute for direct cavity pressure measurement in applications that require it, such as closed-loop pack control, or high-precision medical or optical parts where the manifold-to-cavity pressure relationship cannot be assumed stable. For those use cases a mold-mounted piezoelectric transducer with dedicated signal conditioning is required.
-
-
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "shot.qo",
- "body": {
- "cycle": 247,
- "peak_psi": 1340.5,
- "fill_ms": 760,
- "pack_psi": 890.2,
- "cool_c_s": -1.84,
- "temp_avg_c": 47.3,
- "shot_ms": 28400
- }
-}
-```
-
-## 4. Hardware Requirements
-
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host — runs application code directly with no separate MCU. Exposes ADC, SPI, and I2C via dual 16-pin headers. Includes a short flexible LTE antenna (u.FL); see §5 for cabinet-routing requirements. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular removes per-site IT involvement; WiFi available as an opportunistic fallback. Prepaid SIM with 500 MB and 10 years of service included. |
-| Taoglas CBP.300.11F.200A u.FL-to-SMA female bulkhead pigtail, RG178, 200 mm | 1 | **Required for any metal-cabinet installation** — see §5. Routes the Notecard's cellular u.FL antenna port to an SMA female bulkhead connector at the cabinet wall. The u.FL end snaps onto the Notecard; the SMA female end mounts in a cable gland or conduit knockout. 200 mm is sufficient for most DIN-rail-to-cabinet-wall runs; order Taoglas CBP.300.11F.300A (300 mm) if your enclosure is deeper. Source from Mouser, Arrow, or Taoglas direct (taoglas.com). |
-| External wideband LTE cellular antenna, SMA male, magnetic-mount or direct-mount (e.g. [SparkFun CEL-16432](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html)) | 1 | Mounts on the exterior of the cabinet and screws onto the SMA female bulkhead from the pigtail above. A magnetic-mount whip on the cabinet roof is the most practical field option. Ensure the antenna covers the LTE bands in your deployment region. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter on the power rail for ground-truth energy measurement during bench validation. |
-| [Omega PX309-2KGI](https://www.omega.com/en-us/pressure-strain-force/pressure-transducers/px309/p/PX309-2KGI) hydraulic injection pressure transducer, 0–2,000 PSI, 4–20 mA, 1/4-18 NPT SS | 1 | Strain-gauge transducer with 4–20 mA current-loop output, tapped into a 1/4-18 NPT port on the injection cylinder's hydraulic manifold block — on the hydraulic oil circuit that drives the screw forward, with no contact with the polymer melt. This measures **hydraulic injection pressure** — not in-cavity pressure. 4–20 mA is the plant-floor standard: immune to cable-length voltage drop and noise, and maps directly to the Cygnet ADC through a 150 Ω sense resistor. Because the measurement point is on the hydraulic circuit, no mold modification is required. The 0–2,000 PSI range suits bench and lower-pressure hydraulic circuits; production injection machines commonly run hydraulic injection pressures above this range — upgrade the transducer range to match the circuit and update `max_pressure_psi` accordingly. |
-| [SparkFun Thermocouple Breakout — MAX31855K (SEN-13266)](https://www.sparkfun.com/products/13266) | 1 | SPI thermocouple amplifier IC with cold-junction compensation. Interfaces directly to the Cygnet at 3.3 V; no analog signal conditioning required. |
-| [Omega KMQSS-125G-4](https://www.omega.com/en-us/temperature-measurement/thermocouple-probes/thermocouple-probes-with-transition-junction-connector/kmqss-general-purpose-thermocouple/p/KMQSS-125G-4) K-type thermocouple probe, 1/8" dia. SS sheath, 4" insertion, 1/8-27 NPT | 1 | Standard mold temperature sensor, rated to 1100 °C (well above injection molding range). K-type thermocouples are universal for mold temperatures (25–150 °C typical mold surface). Observe probe polarity: connect the positive lead to TC+ on the MAX31855 breakout — ANSI/US K-type probes use yellow for positive; IEC 60584-3 K-type probes use green. Verify from the probe's lead marking or connector label, not the wire color alone. Select the fitting that matches the mold's thermocouple pocket — M6 bayonet and 1/4-18 NPT adapters are common. **Important:** A 1/8″ sheath in a mold pocket has a thermal response time of several seconds, which is longer than a typical injection-molding cooling phase. This channel is suited for monitoring steady-state mold temperature and detecting gradual multi-shot drift — not for resolving the fast within-shot cooling transient. See §7 for implications on the `cool_c_s` field. |
-| 150 Ω 0.1% precision resistor | 1 | Current-to-voltage conversion for the 4–20 mA loop. At 4 mA this puts 0.60 V on the ADC; at 20 mA, 3.00 V — safely within the Cygnet's 3.3 V ADC range across the full sensor span. |
-| 100 nF ceramic capacitor | 1 | Decoupling capacitor across the 150 Ω sense resistor to suppress switching noise from the transducer loop on the ADC input. |
-| [MeanWell SD-25B-5](https://www.meanwell.com/Upload/PDF/SD-25/SD-25-SPEC.PDF) 24 VDC → 5 VDC/5 A DIN-rail DC-DC converter | 1 | Derives 5 V for the Notecarrier CX from the machine's existing 24 VDC control rail — no separate AC supply needed. |
-| [Hammond Manufacturing 1597DIN4GY](https://www.hammfg.com/part/1597DIN4GY) DIN-rail electronics enclosure, 4-module, ABS, IP20 | 1 | Panel-mount housing appropriate for installation inside an existing electrical cabinet. Slotted DIN-rail clip. Ensure the external antenna cable exits through a cable gland; see §5. |
-
-All Blues parts ship with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-
-Inside the DIN-rail enclosure everything funnels back to the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) and its dual 16-pin header. The Notecard Cell+WiFi (MBGLW) seats into the carrier's M.2 slot, and the rest of the cabinet wiring (the 4–20 mA pressure loop, the SPI thermocouple breakout, and the 5 V step-down from the machine's 24 VDC rail) all comes back to that header. During bench validation, the Mojo sits inline between the 5 V supply output and the Notecarrier's `+VUSB` pad and connects to the **Notecarrier CX's Qwiic connector** so the Notecard can read the Mojo's LTC2959 coulomb counter over the shared I²C bus. The Mojo is bench-validation equipment — it does not go to the field.
-
-
-
-**Electrical safety.** The injection molding machine's electrical cabinet contains hazardous voltages. All wiring and installation work inside the cabinet must be performed by qualified personnel following site lockout/tagout procedures and applicable electrical codes. This system is read-only; it does not command injection, clamp, or any machine motion.
-
-**Hydraulic safety.** Installing the pressure transducer requires opening a port on a high-pressure hydraulic circuit. Before breaking into the hydraulic system:
-1. **Shut down the hydraulic power unit and apply full lockout/tagout (LOTO)** to the hydraulic circuit following the machine OEM's procedure. Do not rely on a control-panel E-stop alone; verify zero pressure at the work port with a calibrated gauge before loosening any fittings.
-2. **Use only a transducer, fitting, and thread sealant rated above the circuit's maximum working pressure and compatible with the hydraulic fluid in use.** Hydraulic injection circuits commonly exceed the 2,000 PSI range of the reference transducer; verify the circuit's maximum pressure and upgrade both the transducer range and fitting ratings to match before installation. An undersized transducer or fitting is a burst and injection-hazard risk.
-3. **Follow the machine OEM's procedure for port access, thread engagement count, and sealant selection.** Use the sealant type specified for the hydraulic fluid (mineral-oil circuits typically use PTFE tape or anaerobic thread sealant; confirm compatibility). Incorrect or excess sealant can contaminate the hydraulic fluid or block the transducer port.
-4. **Torque all fittings to the manufacturer's specification.** After re-pressurizing, **leak-check at full system pressure before returning the machine to service**; inspect the transducer port and all disturbed fittings. Even a pinhole leak in a high-pressure hydraulic line is a serious injection and fire hazard.
-
-
-
-**4–20 mA pressure transducer loop (A0):**
-
-- 24 VDC supply (+) → transducer (+) supply terminal.
-- Transducer (-) return terminal → one end of the 150 Ω sense resistor → GND.
-- ADC input **A0** → junction between transducer (-) and the top of the 150 Ω resistor (i.e., the node that swings 0.60 V–3.00 V with load current). Place the 100 nF capacitor from this node to GND, physically close to the A0 pin.
-- The 24 VDC loop ground and the Notecarrier CX GND must share a common reference: run a ground wire between the machine's 24 VDC return and the Notecarrier's GND pin.
-
-**MAX31855K thermocouple breakout (SPI):**
-
-- MAX31855K breakout `VCC` → `+3V3_OUT` on the Notecarrier CX header (3.3 V, 100 mA available).
-- MAX31855K breakout `GND` → `GND`.
-- MAX31855K breakout `SCK` → `SCK` on the Notecarrier CX header.
-- MAX31855K breakout `DO` (MISO) → `MISO` on the Notecarrier CX header.
-
- > **Notecarrier CX label errata.** The MOSI and MISO silkscreen labels are swapped on v1.3 hardware. If SPI reads return garbage, swap the MOSI/MISO connections and retry. See the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) for the authoritative pin table.
-
-- MAX31855K breakout `CS` → `D10` on the Notecarrier CX header (any digital I/O works; D10 is the firmware default).
-- MAX31855K breakout `TC+` and `TC-` → corresponding positive and negative wires of the K-type thermocouple probe. **Polarity Note:** ANSI/US K-type convention uses yellow for the positive lead; IEC 60584-3 uses green for K-type positive. Consult your probe's lead color or connector marking; do not assume from a generalized color table.
-
-**Antenna (mandatory for metal-cabinet installations):**
-
-The Notecarrier CX includes a short flexible LTE antenna connected to the Notecard's cellular u.FL port. Inside a steel electrical cabinet this antenna will be strongly attenuated by the enclosure walls; reliable cellular connectivity requires the antenna element to be positioned outside the cabinet.
-
-- Thread the u.FL-to-SMA female bulkhead pigtail through a cable gland or conduit knockout in the cabinet wall. Use an IP-rated cable gland to maintain the enclosure's protection rating.
-- Snap the u.FL connector onto the **Notecard's cellular u.FL antenna port**. Keep the coax run short and avoid sharp bends; the RG178 cable is fragile and will fail at a sharp kink.
-- Attach the external SMA wideband antenna to the SMA female bulkhead on the outside of the cabinet. A magnetic-mount whip placed on the cabinet roof is a practical field solution. Keep the antenna clear of large metal objects and away from the high-voltage conductors in the upper section of the panel.
-- Do not use the included flexible antenna inside the cabinet; retain it as a bench-test spare.
-
-
-
-**Field test before commissioning.** RF performance inside a machine cabinet varies by cabinet size, wall thickness, and nearby interference sources. After assembly, verify cloud connectivity by issuing `{"req":"hub.status"}` from the in-browser Notecard terminal or the serial debug port and confirming `connected:true` in the response before locking up the cabinet.
-
-
-
-**Power:**
-
-- 24 VDC rail → 24 V input of the DC-DC step-down converter.
-- DC-DC converter 5 V output (+) → Notecarrier CX `+VUSB` (or `+VBAT`).
-- DC-DC converter 5 V output (−) → Notecarrier CX `GND`.
-- During bench validation: place the Mojo inline between the DC-DC 5 V output and `+VUSB` so it measures the full Notecarrier + Notecard subsystem. Connect the Mojo's Qwiic cable to the **Notecarrier CX's Qwiic connector** (the carrier exposes the shared I²C bus; the Notecard reads the Mojo's LTC2959 over that bus). Remove the Mojo before field deployment; it is a bench-validation tool only.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/injection_molding_shot_monitor/injection_molding_shot_monitor.ino` as `PRODUCT_UID`.
-
-2. **Claim the Notecard.** Power the unit; on first cellular connection the Notecard automatically associates with your project.
-
-3. **Create a Fleet per production line.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. A natural structure is one fleet per production line or cell — all monitors on the same line typically run the same mold and resin and therefore share the same threshold set. Use [Smart Fleet](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) rules to automatically assign monitors by line or mold ID if you have many.
-
-4. **Set environment variables.** All variables below are optional; firmware defaults are shown. Any value set in Notehub overrides the compile-time default on the device's next inbound sync; operators can adjust alert bands in the field to match the specific mold and resin without reflashing.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `max_pressure_psi` | `2000.0` | Full-scale of the installed pressure transducer (PSI). Must match the purchased sensor range. |
- | `shot_detect_psi` | `100.0` | Rising-edge threshold (PSI) above which a shot is considered started. Increase if idle vibration causes false triggers. |
- | `shot_end_psi` | `50.0` | Falling-edge threshold (PSI) below which a shot is considered complete. |
- | `peak_psi_min` | `800.0` | Alert fires when peak injection pressure falls below this value — potential indicator of a short shot, gate freeze, or material flow issue. |
- | `peak_psi_max` | `1900.0` | Alert fires when peak injection pressure exceeds this value — overpacking or flash risk indicator. |
- | `fill_time_min_ms` | `200` | Alert fires when fill time is shorter than this (milliseconds). Unexpectedly short fill times suggest a gate or nozzle issue. |
- | `fill_time_max_ms` | `3000` | Alert fires when fill time exceeds this (milliseconds). Slow fill indicates degraded material flow or low injection speed. |
- | `mold_temp_max_c` | `80.0` | Alert fires when average mold temperature exceeds this value (°C). Rising mold temperature can indicate cooling circuit degradation. |
- | `outbound_min` | `60` | Cellular outbound sync cadence in minutes. When this value changes in Notehub, the firmware detects the difference on its next 5-minute env-var check and immediately re-issues `hub.set` with the new cadence — no reflash needed. Allow up to the `inbound` poll interval (default 120 min) for the Notecard to pull the updated value from Notehub, then up to 5 more minutes for the firmware to apply it. |
- | `report_every_n_shots` | `1` | Emit a `shot.qo` Note every N shots. Set to 10 to reduce data volume on high-speed machines without losing trend visibility. |
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) targeting `shot_alert.qo` (real-time delivery to a quality alert or CMMS endpoint) and a second targeting `shot.qo` (batch delivery to a process historian or analytics platform). Because the two Notefiles are separate at the source, each route operates independently — high-urgency alerts can go to an on-call webhook while trend data lands in a time-series database, all without any filter logic in the routes.
-
-## 7. Firmware Design
-
-The firmware is a single Arduino sketch, [`firmware/injection_molding_shot_monitor/injection_molding_shot_monitor.ino`](firmware/injection_molding_shot_monitor/injection_molding_shot_monitor.ino), organized as a small set of focused functions for Notecard configuration, sensor I/O, shot capture, feature extraction, and Note emission.
-
-**Dependencies:**
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)), installed via the Arduino IDE Boards Manager.
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (the `note-arduino` library). Install via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`.
-
-### Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard setup (`hub.set`, motion disable) | `configureNotecard()` |
-| Template registration for both Notefiles | `defineTemplates()` |
-| Env var fetch and threshold update | `fetchEnvVars()` |
-| 4–20 mA pressure ADC read | `readPressurePsi()` |
-| MAX31855K SPI thermocouple read | `readMoldTempC()` |
-| Shot detection and profile capture | `captureShot()` |
-| Feature extraction from profile buffer | `computeFeatures()` |
-| Shot Note emit (`shot.qo`) | `sendShotNote()` |
-| Alert Note emit (`shot_alert.qo`, `sync:true`) | `sendAlertNote()` |
-
-### Sensor reading strategy
-
-**Pressure.** The 4–20 mA current-loop signal from the transducer passes through a 150 Ω sense resistor, generating a 0.60–3.00 V signal across the Cygnet's 12-bit ADC pin A0. Eight ADC readings are averaged per call to `readPressurePsi()` to suppress switching noise without adding meaningful latency. The resulting count range is mapped linearly from [ADC_4MA_COUNTS … ADC_20MA_COUNTS] to [0 … `max_pressure_psi`].
-
-**Temperature.** The MAX31855K outputs a 32-bit SPI word containing the thermocouple junction temperature in the upper 14 bits (0.25 °C per LSB) and three fault flags in the lower three bits. `readMoldTempC()` reads this word directly over SPI without an additional library dependency, checks the fault bits, and returns `NAN` on any thermocouple wiring fault (open-circuit, short-to-GND, short-to-VCC). `computeFeatures()` guards every temperature accumulation with `isnan()`, so a disconnected or shorted probe does not corrupt arithmetic. When the thermocouple produces no valid samples across a shot, the mold-temperature trend slope (`cool_c_s`) and average-temperature (`temp_avg_c`) fields degrade to `0.0`, a known simplification. `0.0` is an ambiguous sentinel because a mold legitimately near ambient temperature is indistinguishable from a faulty probe in the payload; a future improvement would use a sentinel value outside the physical temperature range (e.g. `-999.0`) so downstream analytics can distinguish the two cases.
-
-**Probe response-time limitation.** A 1/8″ stainless sheath seated in a mold pocket has a thermal response time of several seconds, which is longer than a typical injection-molding cooling phase. As a result, `cool_c_s` (the least-squares slope of temperature during the cooling window of a single shot) primarily reflects slow mold-surface temperature drift across multiple shots rather than the fast within-shot cooling transient. It remains a useful signal for detecting gradual cooling-circuit degradation over time, but it should not be interpreted as a precise shot-level cooling rate. `temp_avg_c` (the mean temperature across the whole captured buffer) is similarly a lagged, averaged reading of the mold surface that is well-suited for steady-state mold temperature trending and the `mold_temp_high` alert threshold.
-
-**Shot capture.** Between shots, `readPressurePsi()` is polled at 10 Hz. When the reading crosses `shot_detect_psi`, the firmware enters `captureShot()`, which samples pressure and temperature at 20 Hz (50 milliseconds cadence) into two pre-allocated float arrays. A 102.4-second maximum shot window at 20 Hz = 2,048 samples × 2 channels × 4 bytes = 16 KB, well within the Cygnet's 64 KB SRAM. `SHOT_TIMEOUT_MS` is defined as `SHOT_BUF_SIZE × SHOT_SAMPLE_MS` (102,400 milliseconds) so the safety timeout fires at exactly the moment the buffer would otherwise fill, preventing a buffer-exhaustion exit from being mistaken for a naturally-ended shot. If the while-condition exits the loop rather than a `break` (buffer completely full), `captureShot()` returns `false` and logs a diagnostic; the partial profile is discarded rather than silently computing features on truncated data.
-
-### Event payload design
-
-Both Notefiles are [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates). Templates tell the Notecard to store and transmit fixed-length binary records rather than free-form JSON, shrinking per-Note wire size by 3–5×. On a machine cycling every 30 seconds across a three-shift day, that's roughly 2,880 shot Notes per day; templates materially reduce per-Note wire size, which helps keep the daily cellular data budget manageable at high cycle rates.
-
-`pack_psi` and `cool_c_s` are recorded in `shot.qo` for downstream trend analysis but do not drive alert rules in this design; only `peak_psi`, `fill_ms`, and `temp_avg_c` are evaluated against configurable alert thresholds.
-
-`shot.qo` records are queued in the Notecard's on-device flash and flushed on the outbound schedule. `shot_alert.qo` is also template-backed (port 51) and is sent with `sync:true`, which instructs the Notecard to skip the outbound queue and open a cellular session immediately for that Note.
-
-Sample `shot.qo` Note body:
-
-```json
-{
- "file": "shot.qo",
- "body": {
- "cycle": 247,
- "peak_psi": 1340.5,
- "fill_ms": 760,
- "pack_psi": 890.2,
- "cool_c_s": -1.84,
- "temp_avg_c": 47.3,
- "shot_ms": 28400
- }
-}
-```
-
-
-
-**`cycle` is a per-boot-session shot sequence number.** `g_cycle_count` is a RAM-only variable that resets to zero on every reboot or power loss. It is not a persistent lifetime part counter. For cumulative production counting, aggregate session counts using the Notehub event timestamp as the ordering key, or implement NVM persistence as a production enhancement (see §10).
-
-
-
-Sample `shot_alert.qo` Note body (immediate sync):
-
-```json
-{
- "file": "shot_alert.qo",
- "body": {
- "alert": "peak_pressure_low",
- "cycle": 248,
- "peak_psi": 692.1,
- "fill_ms": 1240,
- "temp_avg_c": 47.1
- },
- "sync": true
-}
-```
-
-### Power and sync strategy
-
-The injection molding machine is line-powered 24/7, so the host runs a continuous `loop()` rather than cycling through deep sleep. There is no `NotePayloadSaveAndSleep` call; the host must stay responsive between shots to catch the pressure rising edge. Bandwidth efficiency, not sleep depth, is the power concern.
-
-The Notecard runs in [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `periodic` mode with `outbound: 60` and `inbound: 120` (minutes). Shot Notes accumulate in the on-device queue and flush once per hour; alert Notes bypass the queue and sync within a session-establishment window (~15–60 seconds). See the [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) and the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) for authoritative figures.
-
-### Retry and error handling
-
-**Notecard initialization.** `configureNotecard()` issues `hub.set` in a retry loop for up to 5 seconds, calling `requestAndResponse()` on each attempt so the response `err` field can be inspected. A semantic failure, such as an unrecognized `PRODUCT_UID` format, is logged and propagated back to `setup()`, which halts with a `FATAL` message rather than silently proceeding in an unprovisioned state. This replaces the earlier `sendRequestWithRetry()` pattern, which returned only a boolean and could not distinguish a transport timeout from a Notecard-side rejection. If `PRODUCT_UID` is empty (the default), `setup()` halts immediately before attempting any Notecard communication.
-
-**env.get.** `fetchEnvVars()` calls `requestAndResponse()` and guards against both a `nullptr` response (Notecard unreachable) and a response carrying an `err` field (`notecard.responseError(rsp)` returns `true`). In either case the function returns early and the in-RAM thresholds retain their last-good values — the device keeps running on stale (but valid) thresholds rather than stopping.
-
-**Note.template.** `defineTemplates()` runs once at boot and uses `requestAndResponse()` for both `shot.qo` and `shot_alert.qo` registrations, printing the `err` string if either call is rejected. Templates are idempotent: re-running them on the next boot corrects any missed registration, at the cost of one cycle of free-form JSON Notes. The `alert` field in `shot_alert.qo` uses `"peak_pressure_high"` (the longest of the five alert type names, at 18 characters) as its exemplar string so the template allocates sufficient width for all alert values without truncation.
-
-**Note.add.** `sendShotNote()` checks the `sendRequest()` return; a failed `note.add` drops that shot Note, and there is no per-Note retry queue. Lost shot Notes are acceptable at high cycle rates (the process trend is still visible across surviving Notes). `sendAlertNote()` returns `bool` (the `sendRequest()` result). The per-alert cooldown timer in `loop()` is only advanced when `sendAlertNote()` returns `true`, so a transient I2C or Notecard failure does not suppress retries for the full 10-minute cooldown window. The next shot that trips the same condition will attempt the alert Note again immediately.
-
-**card.motion.mode.** The accelerometer-disable call in `configureNotecard()` is best-effort; failure is logged to Serial. A live accelerometer does not affect application correctness; it adds a small amount of idle current to the Notecard's baseline, which is visible on the Mojo trace.
-
-**Sensor faults.** `readMoldTempC()` returns `NAN` on any MAX31855 fault bit. `computeFeatures()` guards every temperature accumulation with `isnan()` so fault samples are silently excluded. A shot with zero valid temperature samples produces `0.0` in the `temp_avg_c` and `cool_c_s` fields. See the sensor reading section for the implications of this sentinel choice.
-
-**Shot capture guard.** `captureShot()` discards events shorter than `MIN_SHOT_DURATION_MS` (500 milliseconds) so that machine vibration or idle pressure noise cannot increment `g_cycle_count` and pollute the trend data.
-
-### Key code snippet 1: template definition
-
-The template registers each field's data type using [Notecard type-hint syntax](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#working-with-note-templates):
-- `14` = 4-byte signed integer (e.g. `cycle`, `fill_ms`, `shot_ms`)
-- `14.1` = 4-byte IEEE 754 float with one decimal place of precision (e.g. `peak_psi`, `pack_psi`, `cool_c_s`, `temp_avg_c`)
-
-Fixed-size binary records let the Notecard store and transmit several hundred queued shots without exhausting its flash, dramatically reducing wire size. `requestAndResponse()` is used so the `err` field is visible if the Notecard rejects a malformed type hint; `sendRequest()` would silently swallow that failure.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "shot.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "cycle", 14);
-JAddNumberToObject(body, "peak_psi", 14.1);
-JAddNumberToObject(body, "fill_ms", 14);
-JAddNumberToObject(body, "pack_psi", 14.1);
-JAddNumberToObject(body, "cool_c_s", 14.1);
-JAddNumberToObject(body, "temp_avg_c", 14.1);
-JAddNumberToObject(body, "shot_ms", 14);
-J *rsp = notecard.requestAndResponse(req);
-if (!rsp || notecard.responseError(rsp)) {
- Serial.print("[APP] note.template (shot.qo) failed");
- if (rsp) { Serial.print(": "); Serial.print(JGetString(rsp, "err")); }
- Serial.println(" — will retry on next boot.");
-}
-if (rsp) notecard.deleteResponse(rsp);
-```
-
-### Key code snippet 2: immediate-sync alert
-
-`sync:true` bypasses the hourly outbound schedule. The Notecard wakes the radio immediately and delivers the alert within a session-establishment window.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "shot_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "peak_pressure_low");
-JAddNumberToObject(body, "cycle", (double)g_cycle_count);
-JAddNumberToObject(body, "peak_psi", peak_psi);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 3: feature extraction — mold-temperature trend slope (cool_c_s)
-
-Because the 1/8″ sheath thermocouple has a thermal response time of several seconds — longer than a typical injection cooling phase — `cool_c_s` does not capture the within-shot cooling transient. What it does capture is the direction and rate of mold-surface temperature drift across the tail end of each cycle window. A least-squares slope is fitted over the post-gate-seal samples in the capture buffer; the result is a °C/s value (negative = mold surface cooling, positive = rising). Used as a trend signal across many shots, a slope drifting toward zero (mold cooling more slowly than baseline) is an early indicator of a fouled or partially-blocked cooling circuit, surfacing the degradation before it shows up in part dimensions or cycle time.
-
-```cpp
-// Linear regression: slope = (N·Σxy − Σx·Σy) / (N·Σxx − (Σx)²)
-// x = time in seconds from gate-seal; y = mold temperature °C
-double sx = 0, sy = 0, sxx = 0, sxy = 0;
-int n = 0;
-for (int i = cool_start; i < g_shot_n; i++) {
- if (isnan(g_temp_buf[i])) continue;
- double x = (double)(i - cool_start) * SHOT_SAMPLE_MS / 1000.0;
- double y = (double)g_temp_buf[i];
- sx += x; sy += y; sxx += x*x; sxy += x*y;
- n++;
-}
-float denom = (float)(n * sxx - sx * sx);
-*cool_c_per_s = (fabsf(denom) > 1e-9f)
- ? (float)((n * sxy - sx * sy) / denom) : 0.0f;
-```
-
-## 8. Data Flow
-
-
-
-**Collected.** During each shot: a continuous time series of injection-manifold pressure (PSI) and mold temperature (°C) sampled at 20 Hz. After each shot: five aggregated features (peak pressure, fill time, pack pressure, mold temperature average, and mold-temperature trend slope) plus total shot duration and a per-boot-session shot sequence number (`cycle`). Pack pressure and the mold-temperature trend slope (`cool_c_s`) are recorded for downstream trend analysis; they do not trigger alerts. Note that `cool_c_s` reflects multi-shot mold-surface temperature drift rather than the within-shot cooling transient. See §7 for the probe response-time limitation, and should be interpreted over a run of shots, not individually.
-
-**Transmitted.**
-- `shot.qo`: one Note per shot (or per N shots if `report_every_n_shots` is set). Queued in the Notecard and synced on the hourly outbound schedule. Template-encoded for wire efficiency.
-- `shot_alert.qo`: emitted only when a feature falls outside its configured alert band, synced immediately via `sync:true`. Each of the five alert types has its own independent 10-minute cooldown timer, so a shot that simultaneously trips multiple conditions (e.g. low peak pressure and high mold temperature) produces a separate Note for each tripped condition. Within any single alert type, at most one Note is emitted per 10-minute window regardless of how many consecutive out-of-spec shots occur.
-
-**Routed.** Both Notefiles go to Notehub. Separate routes can forward `shot_alert.qo` in real time (webhook, email, Slack, CMMS) while `shot.qo` lands in a time-series database or process historian for trend analysis.
-
-**Alert triggers.** Five conditions are evaluated independently per shot:
-
-| Alert | What it signals |
-|---|---|
-| `peak_pressure_low` | Insufficient injection pressure — potential indicator of short shots, gate freeze, or degraded material flow |
-| `peak_pressure_high` | Excessive injection pressure — overpacking, flash risk, or dimensional oversizing |
-| `fill_time_short` | Unusually fast fill — possible gate erosion or runaway injection speed |
-| `fill_time_long` | Slow fill — degraded material flow, low injection pressure, or cold resin |
-| `mold_temp_high` | Mold running hot — cooling system degradation or elevated ambient |
-
-## 9. Validation and Testing
-
-**Expected steady-state cadence.** On a healthy process with default thresholds, the device generates one `shot.qo` Note per injection cycle (e.g., 120 Notes per hour on a 30-second cycle time) and zero `shot_alert.qo` Notes. Expect a commissioning period of 50–100 shots to validate that default thresholds are appropriate for the specific mold and resin; adjusting `peak_psi_min`, `peak_psi_max`, and `fill_time_max_ms` via Notehub environment variables is the intended workflow without touching firmware.
-
-**Simulating alerts during commissioning.** Drop `peak_psi_min` to a value above what the process actually produces (for example, set it to `1500.0` on a process that normally peaks at `1340.0`). The next inbound sync will pull the updated value, and the next shot will trip `peak_pressure_low`; the alert Note should appear in Notehub within a typical cellular session window. Reset the variable to its correct value when done.
-
-**Shot profile spot-check.** During commissioning, set `report_every_n_shots` to `1` and `outbound_min` to `10` in Notehub. After the next Notecard inbound sync (up to 120 minutes at the default cadence) and the next firmware env-var check (up to 5 minutes), the device re-applies `hub.set` and begins syncing every 10 minutes. Each sync delivers a fresh batch of shot Notes. Watch the `fill_ms` and `peak_psi` fields across consecutive shots; on a stable process they should be nearly constant. Gradual drift in `peak_psi` or a step change in `fill_ms` after a material lot change or mold maintenance are exactly the signals this system is designed to surface.
-
-**Data-budget validation (required before production).** With `report_every_n_shots=1` (the default) and a 30-second cycle time, this design can generate up to **2,880 `shot.qo` Notes per day**, plus additional `shot_alert.qo` Notes if thresholds are tripped. Templates reduce per-Note wire size by 3–5× versus free-form JSON, but the raw Note volume is still material at that rate. Before deploying to a production machine:
-
-1. **Estimate your daily Note volume.** Use: `notes_per_day = (86400 s ÷ cycle_time_s) ÷ report_every_n_shots`. A 20-second cycle with `report_every_n_shots=1` produces ~4,320 Notes/day; setting `report_every_n_shots=10` drops that to ~432, still capturing trend signals without a 10× data-volume penalty.
-
-2. **Run a 24–72 hour validation soak.** After commissioning, check [Notehub usage data](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design/#viewing-usage-data-on-notehub) to confirm actual consumption aligns with your estimate. Cellular data usage depends on sync cadence, signal conditions, Note queue depth at sync time, and routing behavior — the measured figure is more reliable than any pre-deployment estimate.
-
-3. **Tune `report_every_n_shots` and `outbound_min` for the machine.** A fast machine (≤15 seconds cycle) running at `report_every_n_shots=1` will accumulate large numbers of queued Notes between hourly syncs. Validate queue headroom empirically before production: disconnect the cellular antenna, let the machine run for the expected outage window (e.g. 4–8 hours for a typical overnight signal gap), reconnect, and confirm that all Notes arrive in Notehub. Missing `cycle` sequence numbers in the delivered Notes reveal dropped queue entries. If your Note rate times the expected outage window risks saturation, increase `report_every_n_shots` or shorten `outbound_min` to flush the queue more frequently. Actual queue capacity depends on template size, Notecard firmware version, and session behavior; the measured result is more reliable than any pre-deployment estimate.
-
-4. **Watch `shot_alert.qo` volume separately.** Each `sync:true` alert Note triggers its own cellular session. The 10-minute per-alert cooldown limits the worst case to 6 sessions per alert type per hour, but if multiple alert types fire simultaneously on a badly off-spec process, session frequency can add up. Check the Notehub event log for alert Note volume during commissioning and confirm that alert thresholds are set appropriately for the specific mold and resin.
-
-**Using Mojo to validate power behavior.** Place the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the 5 V step-down converter and the Notecarrier CX `+VUSB` rail during bench testing. Connect the Mojo's Qwiic cable to the **Notecarrier CX's Qwiic connector** so the Notecard can read the LTC2959 coulomb counter over the shared I²C bus. The Mojo measures the entire subsystem at the 5 V input rail — Notecard plus Notecarrier regulators plus Cygnet host, not the Notecard in isolation.
-
-Published Notecard figures (from the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)). The MBGLW uses an LTE Cat-1 bis modem (Quectel EG916Q-GL) plus a 2.4 GHz 802.11b/g/n WiFi module; the figures below are for the cellular path used in this design:
-
-| Phase | Current at 5 V rail |
-|---|---|
-| Notecard idle — minimum (radio off, deep idle) | < 9 µA — **MBGLW datasheet** |
-| Notecard idle — standby (radio off, between periodic syncs) | ~18 µA — **MBGLW datasheet** |
-| Notecard cellular session (LTE Cat-1 bis, modem active) | ~250 mA average, with brief transmit peaks to ~2 A — **MBGLW datasheet**; session duration typically tens of seconds per hourly sync |
-
-Whole-system figures (Notecard + Notecarrier regulators + Cygnet host at the 5 V Mojo measurement point):
-
-| Phase | Mojo reading (measure on your bench) |
-|---|---|
-| Combined idle (host running, radio off) | The Cygnet's active-run current dominates — measure on your bench; the Notecard's µA-range contribution is negligible relative to the MCU |
-| Shot capture active (ADC + SPI polling at 20 Hz) | Approximately the same as combined idle; ADC averaging and SPI reads add only a small fraction of the MCU's active power |
-
-The Mojo reports cumulative mAh to the Notecard at 1% accuracy over the Qwiic bus. A useful bench exercise: run the device for one hour with no alerts triggering, then confirm the Mojo tally matches the expected pattern: one radio burst per hour lasting tens of seconds at the published ~250 mA, plus continuous host-active current in between. If the radio appears to be syncing far more frequently than the configured outbound cadence, check whether an alert condition is firing `sync:true` Notes in rapid succession; each alert Note triggers an immediate cellular session.
-
-## 10. Troubleshooting
-
-| Symptom | Likely Cause | Fix |
-|---------|--------------|-----|
-| **Notecard won't connect to cellular** | Antenna not attached or misrouted; cabinet shielding too severe | Verify u.FL-to-SMA pigtail is snapped onto the Notecard's cellular port; confirm external antenna is mounted outside the cabinet on clear metal surface. Run `{"req":"hub.status"}` to check `connected` field. |
-| **No shot.qo events appear in Notehub** | ProductUID not set in firmware; Notecard not claimed; shot detection threshold too high | Verify ProductUID in .ino matches Notehub project exactly. Check Notehub device list for your unit and confirm device status is `online`. If present, lower `shot_detect_psi` in Notehub fleet environment and trigger a test pulse. |
-| **Shot.qo events show but shot_alert.qo never fires** | Alert thresholds too loose for actual process; Notecard not issuing immediate sync | During commissioning, artificially tighten thresholds (e.g. set `peak_psi_min` to 1500 on a process that peaks at 1340). Verify the alert fires on the next shot. Use Notehub event log to confirm `sync:true` and cellular session timing. |
-| **Thermocouple reads 0°C or NaN** | Probe disconnected, shorted, or wiring error; MOSI/MISO swapped on Notecarrier v1.3 | Verify MAX31855K CS pin is D10 and SCK/MISO/MOSI match the wiring diagram. Check probe polarity (yellow = positive for ANSI K-type; green for IEC K-type). Swap MOSI and MISO if v1.3 hardware and SPI returns garbage. |
-| **Pressure ADC reads very low or very high** | 150 Ω sense resistor disconnected or wrong value; ADC_COUNTS_AT_4MA / 20MA calibration stale | Measure voltage across A0 with a multimeter during idle and at known test pressure. Verify resistor value with an ohmmeter. Recalibrate ADC constants if a different transducer range was installed. |
-| **Cellular bill much higher than expected** | Alert conditions firing repeatedly, triggering many `sync:true` sessions; `outbound_min` too short | Check Notehub event log for alert spam. Widen alert thresholds or increase the 10-minute per-alert cooldown (`ALERT_COOLDOWN_MS`). Ensure `outbound_min` is at least 30 minutes for production use. |
-| **Serial terminal shows "FATAL: PRODUCT_UID is empty"** | Firmware default PRODUCT_UID not replaced before flashing | Edit firmware/injection_molding_shot_monitor.ino, line ~33: replace `#define PRODUCT_UID ""` with your actual ProductUID, then reflash. |
-| **Mojo coulomb counter reads way too high** | Mojo connected to wrong power rail or Qwiic cable unplugged during soak | Verify Mojo is inline between the DC-DC 5V output and `+VUSB`. Confirm Qwiic cable is snapped firmly to Notecarrier CX Qwiic connector. Reset Mojo by disconnecting and reconnecting power. |
-
-## 11. Limitations and Next Steps
-
-This reference design optimizes for a retrofit that a process engineer can install in an afternoon: no mold modification, no controller modification, no plant-network conversation. A few details were intentionally left simple so that path stays clean; each is documented below alongside the production hardening that closes the gap.
-
-### Simplified for this reference design
-
-Each of the simplifications below is a deliberate scope choice: a place where a production deployment will add a persistent counter, a faster sampler, per-cavity tracking, or richer process logic once the basic single-sensor retrofit is proven.
-
-**Hydraulic injection pressure, not in-cavity pressure.** This design measures hydraulic injection pressure at the injection cylinder's manifold block using a standard 4–20 mA strain-gauge transducer. It does not measure in-cavity pressure, which requires a mold-mounted piezoelectric transducer installed through a dedicated sensor port in the mold (either an existing port or one machined for that purpose). Hydraulic pressure is the upstream forcing function applied by the injection unit; cavity pressure is the actual polymer pressure inside the mold cavity. The two differ by nozzle, gate, and runner losses that vary with resin, temperature, and wear. For shot-to-shot consistency monitoring and trend detection where the hydraulic-to-cavity relationship is reasonably stable, hydraulic pressure is a practical and accessible signal. For applications that require direct cavity pressure (closed-loop pack control, high-precision medical or optical parts), a mold-mounted piezoelectric transducer with appropriate charge amplifier and signal conditioning is required. See the scope note at the top of this document.
-
-**Cycle counter resets on power loss.** `g_cycle_count` is a RAM-only variable. The `cycle` field in every Note is a per-boot-session shot sequence number: **it resets to zero on every power loss or reboot.** It is not a persistent lifetime part counter. For cumulative production counting in a downstream system, use Notehub event timestamps as the canonical ordering key. A production enhancement would persist `g_cycle_count` to the STM32's internal flash (using HAL_FLASH_Program) or to a small external EEPROM, with writes batched (e.g. every 100 shots) to stay well within flash endurance limits.
-
-**Pressure range is POC-level.** The default 0–2,000 PSI range suits benchtop and lab hydraulic circuits but may be insufficient for production injection machines, where hydraulic injection pressures often exceed this range. Deploying on a higher-pressure circuit requires a transducer rated for the actual maximum hydraulic pressure; update `max_pressure_psi` to match and confirm the manifold fitting and transducer pressure ratings before installation.
-
-**20 Hz shot capture is POC-level.** The firmware samples at 20 Hz (50 milliseconds per sample), which is sufficient for extracting the five summary features used here but is too coarse to capture the fine structure of the fill waveform. Fast injection molding machines — particularly those with fill times under 200 milliseconds or rapid gate-seal transients — may require 100–1000 Hz sampling to faithfully characterize fill dynamics and detect peak-pressure spikes. Achieving higher rates on the Cygnet would require SPI DMA, double-buffering, and a deeper profile buffer.
-
-**Profile waveform is not transmitted.** The firmware captures the pressure and temperature profile in RAM and extracts features from it, but only the five features are sent to Notehub. The raw profile arrays are discarded after each shot. A production system that needs SPC waveform analysis or golden-sample comparison would need to transmit the profile itself, but at 2,048 samples × 8 bytes × 2,880 shots per day, transmitting raw profiles is a very different data volume problem.
-
-**Single sensor per shot.** One pressure transducer and one thermocouple. Multi-cavity molds (two-cavity, four-cavity, family molds) would need one transducer per cavity plus a firmware extension to track per-cavity features independently.
-
-**Gate-seal detection is heuristic.** Pack-phase end is detected when pressure drops to 50% of peak. Real-world molds may have a different ratio depending on gate geometry and resin rheology. The `GATE_SEAL_FRAC` constant in firmware is the tuning point for this.
-
-**Shot trigger is pressure-only.** Some machine controllers output a digital shot-in-progress signal on their I/O board. Wiring that signal to a digital input pin and using it as the primary trigger (rather than the pressure threshold) would give more precise shot-boundary timing. The firmware's pressure-threshold approach is a practical alternative for installations where the machine's I/O is not accessible.
-
-**No shot-to-shot baseline tracking.** The alert thresholds are static (set by env var). A production quality system would maintain a rolling baseline for each feature and alert on *deviation from the process baseline* rather than fixed limits, automatically adapting after intentional process changes.
-
-**Mojo is bench-validation only.** The firmware does not read the Mojo's coulomb counter over Qwiic at runtime. Adding a mAh field to the periodic `shot.qo` Note is a straightforward extension if fleet-level energy telemetry becomes valuable.
-
-### Production Next Steps
-
-Once a process engineer is running the basic shot monitor, the following extensions harden it into a production quality tool, roughly from the most immediately useful toward the most analytics-dependent.
-
-**A persistent cycle counter** batch-writes `g_cycle_count` to STM32 internal flash or an external EEPROM every 100 shots so the lifetime shot tally survives power cycling; expose the persisted count as a separate `total_cycle` field in the `shot.qo` Note body.
-
-**Per-cavity monitoring** extends the data model to carry a `cavity_id` field and wires one transducer per cavity; the Notecarrier CX has A0–A5 available for expansion.
-
-**Waveform capture for golden-sample comparison** implements a `TRANSMIT_WAVEFORM` mode (triggered once per N shots or on command from a `_cmd.qi` Notefile) that sends a base64-encoded mini-profile for offline SPC.
-
-**Process change detection** computes an exponentially weighted moving average (EWMA) baseline for `peak_psi` and `fill_ms` on-device and alerts only when a feature deviates from its EWMA by more than a configurable sigma band.
-
-**Field-upgradeable firmware** via [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) lets the OEM push threshold-logic updates across the fleet without a site visit.
-
-**Thermocouple fault-bit handling and internal-reference validation** closes a known gap: the MAX31855 32-bit SPI word contains three fault flags (open-circuit, short-to-GND, short-to-VCC) in bits 2:0 and 12 bits of cold-junction (board-level) reference temperature in bits 15:4 (0.0625 °C per LSB). The firmware checks the fault flags and **excludes faulted temperature samples from the shot averages**, but the shot Note is still emitted regardless: when no valid temperature samples were collected across an entire shot (e.g. a disconnected or shorted probe), `temp_avg_c` and `cool_c_s` both appear as `0.0` in the payload. As noted in §7, this sentinel is ambiguous: it cannot be distinguished from a mold legitimately near ambient temperature. A production improvement would either suppress the shot Note when the probe is entirely faulted, or replace the zero-valued sentinel with a value outside the physical temperature range (e.g. `-999.0`) so downstream analytics can tell a faulted probe from a cold mold. The internal-reference temperature bits are read and discarded; sanity-checking that value against the expected ambient range (e.g. 15–55 °C for a machine room) can catch board wiring faults and cold-junction compensation anomalies before they produce incorrect thermocouple readings.
-
-## 12. Summary
-
-For the process engineer who used to find out about a drifting mold only when scrap rates started climbing, every shot now arrives in Notehub reduced to five interpretable numbers — peak pressure, fill time, pack pressure, average mold temperature, and a cooling slope — with `sync:true` alerts firing the moment any one of them drifts outside its band. A standard 4–20 mA strain-gauge transducer tapped into the hydraulic manifold block and a K-type thermocouple seated in the mold's existing pocket are enough; the mold itself is never touched, and the Notecard's cellular uplink carries the data without ever crossing the plant's OT network. The same hardware SKU and the same Notehub project work at every plant in every geography, so an OEM shipping a process-monitoring module with every new machine — or retrofitting the installed base — gets predictable deployment without an IT ticket or a site survey for any of it.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/injection-molding-shot-to-shot-process-monitor/)**
diff --git a/69-injection-molding-shot-to-shot-process-monitor/banner.png b/69-injection-molding-shot-to-shot-process-monitor/banner.png
deleted file mode 100644
index 009f8f41..00000000
Binary files a/69-injection-molding-shot-to-shot-process-monitor/banner.png and /dev/null differ
diff --git a/69-injection-molding-shot-to-shot-process-monitor/diagrams/01-system-architecture.svg b/69-injection-molding-shot-to-shot-process-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index c8ac1f69..00000000
--- a/69-injection-molding-shot-to-shot-process-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-
diff --git a/69-injection-molding-shot-to-shot-process-monitor/diagrams/02-wiring-assembly.svg b/69-injection-molding-shot-to-shot-process-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 439f6a74..00000000
--- a/69-injection-molding-shot-to-shot-process-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,88 +0,0 @@
-
diff --git a/69-injection-molding-shot-to-shot-process-monitor/diagrams/03-data-flow.svg b/69-injection-molding-shot-to-shot-process-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index e99a0e17..00000000
--- a/69-injection-molding-shot-to-shot-process-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/70-trailer-manufacturer-connected-trailer-platform/README.md b/70-trailer-manufacturer-connected-trailer-platform/README.md
index b2b112f2..82862bc5 100644
--- a/70-trailer-manufacturer-connected-trailer-platform/README.md
+++ b/70-trailer-manufacturer-connected-trailer-platform/README.md
@@ -1,749 +1,5 @@
-# Connected Trailer Platform — Cargo Temperature, Door, and GPS Monitoring
+# Trailer Manufacturer Connected Trailer Platform
-
+A connected-trailer platform for refrigerated trailer OEMs that reports cargo temperature, door events, and GPS position to the manufacturer's cloud over cellular with automatic satellite fallback.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Connected%20Trailer%20Platform%20%E2%80%94%20Cargo%20Temperature%2C%20Door%2C%20and%20GPS%20Monitoring).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/connected_trailer_platform/](firmware/connected_trailer_platform/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a connected-trailer platform for trailer OEM integration, targeting manufacturers who want to own the cellular and satellite connectivity layer on refrigerated trailers from day one. The platform reports the operational signals a fleet operator actually checks (cargo-air temperature at two points inside the trailer, rear-door open/close events, GPS position, and, with vendor-specific decode work, reefer setpoint and tire pressure) back to the OEM's cloud continuously, including across the cellular dead zones common on long rural hauls and at intermodal rail yards. When cellular coverage is unavailable, the device falls back automatically to the [Skylo](https://blues.com/industrial-equipment-monitoring/) non-terrestrial satellite network so the cold-chain record stays continuous through coverage gaps. The hardware is a Blues [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) paired with a [Notecard for Skylo](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) (see §4 for the BOM); the firmware ships three sensor paths fully implemented today (cargo temperature, door, GPS) and two as integration-point stubs awaiting vendor engineering: J2497 reefer telemetry and TPMS tire pressure. The implementation-status callout below summarizes all five paths.
-
-## 1. Project Overview
-
-**The problem.** A refrigerated trailer is a one-time sale that lasts a decade or more. Once it leaves the factory, the manufacturer's recurring revenue opportunity hinges entirely on what services they can sell over that trailer's lifetime, and the telematics layer is the obvious candidate. The trouble is that battle is already being fought on every refrigerated trailer on the road. Thermo King and Carrier, the two dominant **reefer** (refrigeration unit) OEMs, ship their own fleet connectivity built around the reefer unit's proprietary data ports. Aftermarket platform vendors layer on top of those. By the time a trailer reaches a fleet operator's yard, two or three telematics competitors are already reading its temperature and location, and they all have a head start.
-
-A trailer OEM that wants to own the connectivity layer, rather than ceding it to the reefer OEM, needs a hardware-and-software platform that's on the trailer from day one. That platform has to read everything the fleet operator actually cares about: the reefer setpoint and actual temperature from the refrigeration unit itself, body-air temperature at two points in the cargo space (the data the shipper trusts at delivery time, not the reefer unit's own sensor), tire pressure on every axle, rear-door events that tell the cold-chain story, and GPS for asset location and route compliance. It has to survive 10–15 years of operation across regions, carriers, and signal environments, including long rural hauls, intermodal drayage moves, and multi-day DC (**distribution center**) dwells where the trailer sits disconnected from the tractor for days at a stretch, often in areas where cellular coverage is unreliable.
-
-**Why Notecard.** A trailer OEM cannot commit to a modem they might have to re-source in year four of a ten-year program. Notecard for Skylo solves this at the module level: it combines LTE-M/NB-IoT cellular and Skylo satellite (via **NTN**, Non-Terrestrial Network, meaning communication through geostationary satellites rather than cell towers) in a single pre-certified M.2 form-factor module with an embedded SIM, 500 MB of cellular data, and 10 years of service included. The OEM installs one hardware SKU, flashes one firmware image, and ships trailers into any regional market without per-unit carrier activation, without SIM swaps, and without a per-site recurring data-plan negotiation. When a trailer crosses into a cellular dead zone (a mountain pass, a rural cold-storage depot, a remote intermodal rail yard), Notecard for Skylo transitions transparently to the Skylo satellite network, so the cold-chain record and the TPMS safety log stay continuous. The [Blues Notehub](https://blues.com/notehub/) cloud service gives the OEM's cloud a single API endpoint regardless of which radio delivered any given message. That's the connectivity bundle a trailer manufacturer needs to compete on the data layer rather than concede it to the reefer OEM by default. Pre-certified global cellular plus satellite plus included data, all in one module with a credible long-term roadmap, is exactly the program-level assurance a trailer OEM needs.
-
-
-
-**Deployment scenario.** A weatherproof enclosure mounted on the trailer's nose wall (the forward interior wall, adjacent to the reefer unit). Notecard for Skylo's Skylo-certified flat-patch antenna is exterior-mounted on the trailer roof with a clear sky view; the GPS antenna is also exterior on the roof. **SAE J2497** (also marketed as **PLC4TRUCKS**) is the SAE standard for power-line communications over the existing power conductors of commercial-vehicle trailer wiring; the J2497 PLC signal rides on the **J560 pin 6 circuit**, the always-hot auxiliary/battery feed on the SAE J560 North American seven-pin trailer connector; not a stop-lamp or brake circuit. The reference build includes a documented integration point for a future J2497 coupling interface on that circuit, and the firmware scaffolding routes a POC placeholder through the data pipeline, but this is not a delivered sensor path. A practical caveat: J2497's dominant production use is trailer ABS warning-lamp telemetry, not reefer data. Most reefer-OEM telemetry in the field is delivered over vendor-proprietary serial diagnostic ports (Carrier Transicold DataLink, Thermo King DSR/DSR2) or J1939 over CAN, not over J2497. The firmware UART (`Serial1`) is the same interface either way; only the decode function changes, so the reference assumption can be re-targeted to a different transport without rearchitecting the platform. See [§10 Limitations](#10-limitations-and-next-steps) for the alternative-transport comparison and the engineering required to activate any of them. **Regional Note:** SAE J560 is the North American 7-pin standard; European trailers use ISO 1185 (7-pin) or ISO 3731 (13-pin) connectors with different pinouts; J2497 PLC is deployed on the equivalent auxiliary circuit in those standards. Two encapsulated NTC thermistors hang inside the cargo space, front and rear, providing the independent body-air readings shippers use to verify cold-chain integrity at delivery. A documented integration point for a future TPMS (**Tire Pressure Monitoring System**) gateway receiver is included; the firmware scaffolding and note-template fields for four tire positions are in place, but this is not a delivered sensor path; real tire pressure data requires the vendor-specific engineering described in [§10 Limitations](#10-limitations-and-next-steps). A magnetic reed switch on the rear door reports open/close state. The Notecarrier CX and its onboard Cygnet STM32 host run the trailer state machine; Notecard for Skylo handles all radio management, GNSS positioning, and Notehub data delivery. Power during tractor-connected operation comes from the trailer's 12 V auxiliary/battery circuit, **J560 pin 6** (the always-hot auxiliary feed, distinct from the stop-lamp and brake circuits); during DC dwells when the tractor is disconnected, a power-priority switching circuit transfers automatically to the reefer unit's own 12 V battery (integral to every refrigerated trailer), keeping the power path alive during DC dwells. The host uses `NotePayloadSaveAndSleep` / `card.attn` to sleep at zero current between sample cycles, making multi-day DC-dwell operation within a practical reefer battery budget achievable; see [§7](#7-firmware-design) for the power architecture detail.
-
-
-
-**Platform sensor paths: implementation status**
-
-**Fully implemented and buildable today:**
-
-- Two NTC cargo-air thermistors with β-equation ADC conversion and window-aggregate min/max/mean statistics
-- Rear-door reed switch with open-time accumulation and distinct event counting
-- GPS asset tracking via Notecard built-in GNSS with motion-state detection and cadence switching
-- Notecard for Skylo (NOTE-NBGLWX) with automatic LTE-M/NB-IoT cellular → Skylo NTN satellite fallback
-- Templated binary summary Notes (`trailer_summary.qo`) and immediate-sync alert Notes (`trailer_alert.qo`)
-- Notehub environment-variable–driven threshold and sync-cadence configuration, updateable over the air
-- Dwell-capable host sleep via `NotePayloadSaveAndSleep` / `card.attn`: host draws zero current between sample cycles; the Notecard's radio-idle floor (~8–18 µA) is the only static draw during sleep
-
-**Integration-point stubs (require additional vendor engineering to activate):**
-
-- **Reefer telemetry over a serial UART (reference assumption: J2497 PLC):** The firmware reserves `Serial1` for a future reefer-telemetry source and parses a simplified POC frame through the full alert/summary pipeline. The reference design assumes a power-line-carrier link based on **SAE J2497 / PLC4TRUCKS** using a Yitran IT700 modem, but it's worth understanding upfront that J2497's dominant deployed use is trailer ABS warning-lamp telemetry, and reefer telemetry specifically over J2497 is uncommon in production. Most reefer telemetry in the field rides on **vendor-proprietary serial diagnostic ports** (Carrier Transicold DataLink over RS-232/RS-485, Thermo King Direct Smart Reefer / DSR2) or **J1939 over CAN**. The firmware UART can be re-targeted to any of those by replacing the decode function in `drainReeferUart()`; only the transport changes, not the data pipeline. Activating any path requires (a) selecting the transport, (b) the corresponding application-layer stack and reefer-OEM message mapping, and (c) field validation. The `reefer_sensor_loss` alert is gated so it does not fire on a build with no reefer-telemetry source connected. See [§10](#10-limitations-and-next-steps).
-- **TPMS tire pressure:** The firmware parses a generic POC packet format and routes pressures through the alert/summary pipeline, but production firmware must replace the parser with the chosen vendor's proprietary decode library. See [§10](#10-limitations-and-next-steps).
-
-
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Each time the Notecard's ATTN signal brings the Cygnet STM32L433 back to life, the host walks the three fully-implemented sensor inputs — both cargo-air thermistors (12-bit ADC with β-equation conversion), the rear-door reed switch, and the GPS position from the Notecard's built-in GNSS — and then services the two UART stub channels: `Serial1` for the J2497 reefer path and `tpmsSerial` for the TPMS path. Both UART parsers handle simplified POC frame formats; neither produces real trailer data without the additional engineering described in §10. With every sample the host runs the threshold rules locally and decides whether the cycle warrants an immediate alert or just feeds the running summary accumulator. Between cycles the host is powered off entirely via the Notecard's ATTN signal (see [§7 Low-power strategy](#7-firmware-design)); every byte of accumulated window state rides through the sleep in a serialized `PersistState` payload stored in the Notecard. On each wakeup, before the sample cycle starts, the host gives both UART channels a 250 ms drain to mop up frames that arrived during sleep, then drains again between each blocking Notecard I²C transaction so a long radio call doesn't shadow an incoming UART frame. All Notecard communication rides I²C over the Notecarrier CX's internal routing; the host never touches the radio or the cellular session.
-
-**Notecard responsibilities.** From there Notecard for Skylo takes over. It keeps [Notes](https://dev.blues.io/api-reference/glossary/#note) in its on-device queue, picks between cellular and satellite radios automatically, and flushes the queue on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) outbound cadence the firmware sets for the current trailer state: shorter in transit, longer during DC dwells. Alert Notes marked `sync:true` skip the outbound timer entirely and bring up a radio session right away. GNSS is also the Notecard's job, and every Note that leaves the device is automatically tagged with the last known position. The same channel runs the other direction for [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) pushed from Notehub, so fleet operators can retune thresholds and sync cadences across an entire customer's trailers without anyone touching firmware.
-
-**Notehub responsibilities.** [Notehub](https://dev.blues.io/notehub/notehub-walkthrough/) is where the data lands. Notes arrive over the Internet, every event is stored, and project-level [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) fan them out to wherever the OEM's cloud needs them. The same firmware image adapts to wildly different cargo types through fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/): a frozen-goods customer and a produce customer differ only in the reefer temperature bands their fleet encodes, not in their device build. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) let the OEM slice the installed base by customer, cargo class, or route region without ever forking the firmware.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-**What you'll have when you're done:** A Notecarrier CX running templated sensor data to Notehub's cloud, with temperature and door-open alerts firing immediately when thresholds trip. No J2497 reefer or TPMS data yet; those are integration stubs, but the three fully-implemented paths (cargo temperature, door, GPS) are ready to validate the platform on real hardware.
-
-**Time estimate:** 30 minutes to flash and commission on the bench; add 15 minutes if you're wiring thermistors for the first time.
-
-1. **Install firmware dependencies.**
- - Install the Arduino core for STM32: In the Arduino IDE, go to **Boards Manager**, search `STM32`, and install `STM32 Cores by STMicroelectronics`.
- - Install the Blues Wireless Notecard library: Search `Blues Wireless Notecard` in the **Library Manager**, and install the latest version.
- - Via CLI: `arduino-cli core install STMicroelectronics:stm32` and `arduino-cli lib install "Blues Wireless Notecard"`.
-
-2. **Get your ProductUID and paste it into the sketch.**
- - Sign up at [notehub.io](https://notehub.io), create a project, and copy your [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid).
- - Open `firmware/connected_trailer_platform/connected_trailer_platform.ino` and paste the ProductUID into the empty `PRODUCT_UID` string at the top.
-
-3. **Flash the sketch to Notecarrier CX.**
- - Plug the Notecarrier CX (with Notecard for Skylo seated in the M.2 slot) into your computer via USB.
- - Select **Board: Blues Cygnet** in the Arduino IDE (the Notecarrier CX's embedded host is the Blues Cygnet, STM32L433-based).
- - Click **Upload** (or via CLI: `arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET --upload firmware/connected_trailer_platform/`).
-
-4. **Apply power and watch Notehub for data.**
- - Plug a 5 V USB power supply into the Notecarrier's micro-USB port (for bench testing; field units draw power from the 12 V trailer supply via the DC-DC converter).
- - Open Notehub and navigate to **Devices**. The Notecard associates automatically on first powerup.
- - Within 1–5 minutes (depending on cellular coverage), a `_session.qo` Note appears confirming the device's cellular registration.
- - Every 5 minutes (default `sample_interval_sec`), a `trailer_summary.qo` event lands in your project with temperature and door fields populated. Without thermistors wired, the temperature fields show −9999.
-
-5. **Wire the three sensors (optional for bench demo).**
- - Two 10 kΩ NTC thermistors with 10 kΩ 1% series resistors on ADC pins A0 and A1 (see [§5 Wiring](#5-wiring-and-assembly) for voltage-divider schematic).
- - A magnetic door reed switch on GPIO D9 (pull-up enabled in firmware).
- - GPS and cellular/satellite antennas: already included with Notecard for Skylo.
-
- With sensors wired, `trailer_summary.qo` reports real `air_t1_*` and `air_t2_*` fields and tracks `door_open_min` and `door_event_count`.
-
-6. **Configure fleet environment variables (optional, defaults work out of the box).**
- - In Notehub, navigate to **Fleets** and create one (or use the default).
- - Assign your device to the fleet.
- - Add **Environment Variables** to tune the thresholds and sync cadences (see [§6 Notehub Setup, step 4](#6-notehub-setup) for the full variable list).
- - Changes appear on the device at the next inbound sync, no re-flash needed.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "reefer_set_f": -9999.0,
- "reefer_min_f": -9999.0,
- "reefer_max_f": -9999.0,
- "reefer_mean_f": -9999.0,
- "air_t1_min_f": 35.6,
- "air_t1_max_f": 37.2,
- "air_t1_mean_f": 36.3,
- "air_t2_min_f": 35.1,
- "air_t2_max_f": 36.8,
- "air_t2_mean_f": 35.9,
- "door_open_min": 0.0,
- "door_event_count": 0,
- "tpms_0_psi": -9999.0,
- "tpms_1_psi": -9999.0,
- "tpms_2_psi": -9999.0,
- "tpms_3_psi": -9999.0,
- "tpms_0_age": 1,
- "tpms_1_age": 1,
- "tpms_2_age": 1,
- "tpms_3_age": 1,
- "trailer_state": 2,
- "sample_count": 12
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with embedded Cygnet STM32L433 host — no separate MCU needed. M.2 slot seats Notecard for Skylo. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | 1 | LTE-M/NB-IoT cellular + Skylo NTN satellite + integrated GNSS in one pre-certified module. Single SKU for any region; automatic cellular-to-satellite fallback with no host-firmware involvement. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter for power-envelope validation during bench commissioning. |
-| 10 kΩ NTC thermistor, β≈3950, encapsulated probe ([Adafruit 372](https://www.adafruit.com/product/372)) | 2 | Cargo-air temperature at two points — front and rear of the trailer. These are the independent readings shippers use to verify cold-chain integrity at delivery, separate from the reefer unit's own sensor. |
-| 10 kΩ 1% resistor | 2 | Voltage-divider series resistor for each thermistor on A0 and A1. |
-| Magnetic door sensor, N.C., panel-mount ([Adafruit 375](https://www.adafruit.com/product/375)) | 1 | Rear door open/close state on D9. When the door is **closed** the magnet holds the N.C. contact open, leaving the pull-up to drive the pin HIGH (`doorOpen = false`). When the door **opens** the magnet moves away; the N.C. contact closes to GND and pulls the pin LOW (`doorOpen = true`). |
-| Skylo-certified flat-patch antenna with u.FL lead (included with Notecard for Skylo) | 1 | Connects to the **`MAIN`** u.FL port on Notecard for Skylo. This single included antenna covers both LTE-M/NB-IoT cellular and Skylo NTN satellite (S-Band/L-Band, B23/B255/B256) — no separate cellular antenna is needed or installed. Exterior roof-mount, flat face toward the sky with ≥11 mm clearance on all edges. Replacing or modifying this antenna, or inserting additional RF connectors into the antenna lead, voids the Skylo network certification and may cause Skylo to block the device. Route the included lead through an IP68-rated cable gland (see §5) — **not** through an added RF connector or bulkhead adapter. |
-| Passive GPS/GNSS antenna, u.FL, exterior-mount ([**Taoglas AA.162**](https://www.taoglas.com/product/ulysses-aa-162-miniature-magnetic-mount-gps-glonass-antenna/) passive patch, −40 to +85 °C) | 1 | Connects to the **`GPS`** u.FL port on Notecard for Skylo. The NOTE-NBGLWX GPS port requires a **passive** (un-amplified) antenna — the port does not supply DC bias for an active antenna's LNA. Roof-mount with clear sky view; route the coax through a weatherproof u.FL-to-SMA bulkhead feedthrough. |
-| DC-DC converter, 9–36 V input, 5 V/3 A output ([Mean Well SD-15A-5](https://www.meanwell.com/Upload/PDF/SD-15/SD-15-SPEC.PDF)) | 1 | Steps down the 12 V supply (from tractor auxiliary line when connected, or reefer battery during DC dwells) to the 5 V rail powering the Notecarrier CX. The SD-15A-5's 36 V maximum input provides margin against the trailer's normal transient envelope above 12 V nominal. |
-| Power-priority OR circuit: **ON Semi MBRS340T3G** automotive Schottky diode, 3 A / 40 V | 2 | One diode per 12 V input leg (tractor aux and reefer battery), anodes to their respective inputs, cathodes joined to a common output node feeding the DC-DC converter VIN. Automatically passes the higher-voltage source with no host involvement; the Schottky's reverse-blocking inherently protects against polarity reversal on each input. Forward drop ~0.4 V at full load — acceptable for a 12 V rail. For production a low-drop ideal-diode controller (e.g., TI LM74700-Q1, AEC-Q100) cuts forward drop to millivolts but adds FETs and layout complexity. **Do not use low-voltage power mux ICs** (e.g., TPS2116, max 5.5 V) — trailer supply and load-dump transients far exceed their rating. |
-| IP68 polycarbonate enclosure, ≥160×120×60 mm ([**Hammond 1555H2GY**](https://www.hammfg.com/part/1555H2GY), 180×120×60 mm, light gray, IP68) | 1 | Nose-wall mount; rated against refrigeration condensation and high-pressure washing during trailer cleaning. Key selection criteria: IP67 or better, polycarbonate or ABS body (survives trailer wash-down chemicals), continuous service to −40 °C, ≥160 mm internal length to accommodate the Notecarrier CX with antenna feedthrough headers, cable-gland knockouts on at least one long face. |
-| Weatherproof inline blade fuse holder + 7.5 A slow-blow ATC/ATO automotive blade fuse | 2 | One per 12 V input leg (J560 pin 6 tractor aux and reefer battery). Fuse as close to the source as practical — before any other component on each leg. Slow-blow rating withstands DC-DC converter inrush. |
-| Automotive transient protection — Stage 1: **Bourns MOV-20D201K** metal-oxide varistor, 20 V RMS, 26 V DC, 400 J energy rating, 20 mm disc, radial leads, THT | 2 | One per 12 V input leg (tractor-aux and reefer-battery), installed from the fused hot wire to chassis GND immediately before each OR-circuit Schottky diode anode. ISO 7637-2 pulse 5b load-dump at a 12 V system's nominal 4 Ω source impedance delivers at most ≈ 190 J per event; the 400 J energy rating provides ≥ 2× thermal margin per device. The 7.5 A slow-blow fuse on each leg limits the peak surge current into the MOV — keep the fuse-to-MOV wiring as short as practical to maximize impedance in the surge path. |
-| Automotive transient protection — Stage 2: **Vishay SMBJ26CA** bidirectional TVS diode, 26 V standoff, 600 W peak, DO-214AA (SMB package) | 1 | Installed from the OR-circuit common cathode output node to chassis GND, between the Schottky cathode junction and the SD-15A-5 VIN. Clips fast inductive-kick transients (µs timescale) that the MOV's finite reaction time does not fully arrest. Together with the two upstream MOVs, this two-stage design holds the SD-15A-5 VIN within its 36 V maximum input under typical 12 V trailer ISO 7637-2 conditions. For installations with very short, low-resistance wiring to a high-output alternator — where source impedance is substantially below 4 Ω — verify MOV peak surge-current margin against the Bourns MOV-20D201K datasheet; add a TI LM74721-Q1 (AEC-Q100) surge-stopper IC with an external ≥ 100 V N-channel MOSFET as a third stage between the Stage 1 and Stage 2 elements if warranted. |
-| IP68 cable gland sized for the Skylo flat-patch antenna lead (e.g., M16 or M20 IP68 nylon cable gland) | 1 | Routes the included Skylo-certified antenna lead through the enclosure wall without adding any RF connector to the certified cable path. Thread the lead through the gland and tighten against the cable jacket. Do **not** use a u.FL-to-SMA bulkhead adapter or any other RF connector on this lead — adding connectors to the certified cable/antenna assembly changes the approved RF path and voids Skylo certification. |
-| u.FL to SMA panel-mount bulkhead feedthrough adapter, IP67, stainless steel or nickel-plated brass body, with SMA-to-u.FL pigtail | 1 | For the GPS passive patch antenna coax (Notecard for Skylo **GPS** port) only. The GPS antenna is not part of the Skylo RF certification; a standard weatherproof RF feedthrough is appropriate here. Do **not** use this type of assembly on the Skylo flat-patch antenna lead (MAIN port). |
-| SAE J560 pin 6 wire tap — weatherproof butt-splice or ring terminal, rated for 10–12 AWG wire | 1 | Taps the trailer's existing pin 6 (auxiliary/battery circuit) wire at or near the nose-wall J560 socket to feed the inline fuse holder and then the OR circuit Input A. Use a crimp-sealed weatherproof butt-splice connector or a ring terminal on the appropriate stud; do not use insulation-displacement taps on trailer supply wiring. |
-
-*All Blues hardware ships with a pre-activated SIM, 500 MB of cellular data, and 10 years of service — no activation fees, no monthly commitments.*
-
-### Future Integration Hardware
-
-The following items are **not part of this reference build** and should not be purchased or assembled until the corresponding integration engineering is complete. The firmware includes stub parsers for both paths as placeholders; see [§10](#10-limitations-and-next-steps) for the full engineering scope required before either produces real trailer data.
-
-| Part | Qty | Notes |
-|------|-----|-------|
-| **J2497 PLC modem custom board**: [Microchip (Yitran) IT700](https://www.microchip.com/en-us/product/IT700) (note: IT700 is legacy; Microchip's IT900A is the current backward-compatible successor) PLC modem IC + Bourns PT60234PEL bus-coupling transformer + bus-clamping TVS diodes and ferrite filter on a custom PCB | 1 | No off-the-shelf UART-output J2497 module exists; the custom board resolves only the physical modem layer. Three additional engineering layers are required before any reefer data flows: (a) a licensed J2497 application-layer protocol stack, (b) reefer-OEM message mapping under a vendor agreement, and (c) field validation. See [§10](#10-limitations-and-next-steps) and [Future Integration: J2497](#future-integration-reefer-telemetry-uart-reference-j2497-plc-modem-path). |
-| **TPMS OEM gateway**: PressurePro CORE OEM Module (or equivalent), 315 MHz, 12 V DC, serial data output | 1 | Designed for trailer OEM integration with a serial data port. The firmware's TPMS parser is a POC stub that models a generic one-frame-per-tire format no commercial gateway produces. Production firmware must replace the parser with the chosen vendor's protocol decode library. See [§10](#10-limitations-and-next-steps) and [Future Integration: TPMS Receiver Path](#future-integration-tpms-receiver-path). |
-
-## 5. Wiring and Assembly
-
-
-
-Inside the nose-wall enclosure, everything traces back to the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) and its dual 16-pin headers. Notecard for Skylo seats into the carrier's M.2 slot, and the rest of the trailer-side wiring — thermistor leads, door reed, J560-pin-6 supply, antenna pigtails — comes back to that header from the rest of the box. Two routing rules are worth calling out upfront because they're easy to get wrong: the GPS antenna coax exits the enclosure through the weatherproof u.FL-to-SMA bulkhead feedthrough, while the Skylo MAIN antenna lead must pass through an IP68 cable gland only — never through a bulkhead adapter or any additional RF connector (see BOM and the MAIN u.FL bullet below). The Mojo sits inline between the DC-DC converter's 5V output and the Notecarrier's +VBAT pad during bench power validation (remove it from the field unit once commissioning is complete, or leave it in place if fleet-level energy telemetry is desired). In field installations the DC-DC converter's VIN is fed from the power-priority switching module described in the power chain below, not directly from J560 pin 6.
-
-Pin-by-pin:
-
-- **12V trailer supply chain (power inputs, field installation).** J560 **pin 6** (trailer auxiliary/battery circuit, always-hot when tractor is connected; SAE J560 North American standard) → 7.5 A slow-blow inline fuse + fuse holder → **Bourns MOV-20D201K to chassis GND** (Stage 1 transient clamp, one per leg) → **OR circuit Input A** (the MBRS340T3G Schottky diode in the OR circuit inherently blocks reverse-polarity on this input). **OR circuit Input B:** reefer unit battery positive → 7.5 A slow-blow inline fuse + fuse holder → cable run to nose-wall enclosure → **Bourns MOV-20D201K to chassis GND** (Stage 1, Input B leg) → **OR circuit Input B**. The reefer battery is the only Input B source documented in this design; a standalone dedicated telematics battery is an alternative used in some field deployments but requires a charge controller, float charger, and low-voltage disconnect — do not substitute a bare battery without those elements or it will eventually fail from deep cycling. **OR circuit common cathode output** → **Vishay SMBJ26CA to chassis GND** (Stage 2 TVS, fast inductive-spike clamp) → SD-15A-5 VIN. DC-DC converter VOUT (5 V) → Mojo BAT → Mojo LOAD → Notecarrier CX **+VBAT**.
-- **MAIN u.FL** port (Notecard for Skylo) → the included Skylo-certified flat-patch antenna lead. This single antenna covers both LTE-M/NB-IoT cellular and Skylo NTN satellite (S-Band/L-Band, B23/B255/B256) — no separate cellular antenna is needed. Thread the included lead through an IP68 cable gland (see BOM) in the enclosure wall; cinch the gland against the cable jacket to maintain the IP68 enclosure rating. Lay the flat-patch antenna flat face-up on the trailer roof with ≥11 mm clearance on all sides. **Do not insert a u.FL-to-SMA adapter, SMA bulkhead, pigtail, or any other RF connector into this antenna lead** — adding connectors to the certified cable/antenna assembly changes the approved RF path and voids Skylo network certification.
-- **GPS u.FL** port (Notecard for Skylo) → passive GPS/GNSS antenna lead, routed through the u.FL-to-SMA bulkhead feedthrough (see BOM) to an exterior roof-mount passive patch antenna (e.g. Taoglas AA.162). The NOTE-NBGLWX GPS port does not supply DC bias; **active (amplified) antennas are not compatible** with this port. Mount with unobstructed sky view.
-- **+3V3 (out)** → high-side lead of each NTC thermistor probe (two separate voltage dividers, one per probe, wired **+3V3 → NTC probe → ADC node → 10 kΩ series resistor → GND**; the NTC is on the high side so the firmware's β-equation formula, `R = R_series × (ADC_MAX / raw − 1)`, computes the correct thermistor resistance). Note: when a J2497 custom board is eventually built, its IT700 logic VCC also comes from this +3V3 rail, keeping that current on the Mojo trace. See the [Future Integration: J2497 PLC Modem Path](#future-integration-reefer-telemetry-uart-reference-j2497-plc-modem-path) subsection below.
-- **GND** → low side of each 10 kΩ series resistor (divider low leg), one lead of the door sensor. Route signal grounds away from the trailer's chassis ground to avoid noise pickup on the UART channels.
-- **A0** → wiper of thermistor 1 voltage divider (front probe, suspended midway up the nose wall, away from the reefer discharge coil).
-- **A1** → wiper of thermistor 2 voltage divider (rear probe, suspended midway up the rear door jam).
-- **D5 / D6** → *(future integration, no external connection required on the current reference build)* SoftwareSerial TX/RX are initialized in firmware as integration-point stubs for the TPMS receiver. With no gateway connected these pins receive no data and all TPMS positions report −9999. See [Future Integration: TPMS Receiver Path](#future-integration-tpms-receiver-path) below for wiring when a TPMS gateway is added.
-- **D9** → one lead of the magnetic door sensor (the other lead ties to GND); firmware enables the STM32's internal pull-up. When the door is **closed**, the magnet holds the N.C. contact open; the GND path is broken and the pull-up drives the pin **HIGH** (`doorOpen = false`). When the door **opens**, the magnet moves away; the N.C. contact returns to its closed (conducting) state, creating a GND path that pulls the pin **LOW** (`doorOpen = true`).
-- **SDA / SCL** → routed internally on the Notecarrier CX between the Cygnet host and the Notecard; no external wiring required for I²C communication.
-- **+VBAT** → Mojo LOAD output (bench commissioning); Mojo BAT input ← 5V from the DC-DC converter VOUT. See power chain in the power inputs bullet above for the full supply path from trailer 12V to +VBAT.
-
-Mount the enclosure on the nose wall at a height that keeps all connectors above the refrigerant condensation zone and the floor wash zone (typically ≥300 mm above floor). Suspend thermistor probes on nylon tie-mounts anchored to the interior wall studs: T1 at the front quarter of the trailer, T2 at the rear quarter near the door jam, both hanging clear of any cargo contact. Route the GPS antenna coax through the enclosure wall via the u.FL-to-SMA bulkhead feedthrough; route the Skylo MAIN antenna lead through the IP68 cable gland only; do not use the bulkhead feedthrough or any RF connector on the MAIN lead. Lay the Skylo flat-patch antenna flat face-up on the trailer roof with ≥11 mm clearance on all sides. Do not run antenna coax parallel to 12 V DC supply wires; maintain ≥50 mm separation or cross at 90° where proximity is unavoidable.
-
-**Power input protection.** Commercial trailer wiring is electrically harsh: J560 load-dump transients can reach 87 V peak under ISO 7637-2 pulse 5b conditions, and polarity reversal is a common field error. The fuse holders + 7.5 A slow-blow fuses and the MBRS340T3G OR-circuit Schottky diodes (which inherently block reverse polarity on each input leg) in the §4 BOM are non-negotiable. The Schottky OR circuit handles dual-source priority selection and reverse-polarity protection but is not a transient clamp. The two-stage protection specified in §4 provides the transient suppression: one **Bourns MOV-20D201K** per input leg (installed from the fused hot wire to chassis GND before the Schottky anode) absorbs the bulk load-dump energy: at 4 Ω source impedance a pulse 5b event delivers at most ≈ 190 J, within the MOV's 400 J rating with margin. The **Vishay SMBJ26CA** bidirectional TVS on the common OR-circuit cathode output node clamps the residual fast inductive-kick transients before they reach the SD-15A-5 VIN (36 V maximum input). Together the two stages hold the converter input within its rating under the nominal 4 Ω source-impedance scenario. For installations with very short, low-impedance wiring near a high-output alternator, verify MOV peak surge-current margin against the Bourns datasheet and consult the TI LM74721-Q1 application Notes if an active third stage is warranted.
-
-### Future Integration: Reefer Telemetry UART (Reference: J2497 PLC Modem Path)
-
-
-
-**This subsection documents future integration work, not the current reference build.** `Serial1` is reserved in firmware for a reefer-telemetry source. The reference assumption is a J2497 PLC modem because it preserves the J560-only physical interface to the trailer, but the IT700 coupling board does not exist and the J2497 application stack is not licensed. Include this wiring only when those two pieces are in place.
-
-**Before committing to J2497, read the alternatives.** J2497 / PLC4TRUCKS is the SAE standard for trailer power-line communications, but its dominant deployed use is ABS warning-lamp telemetry. Production reefer telemetry typically rides on a vendor-proprietary serial diagnostic port (Carrier Transicold DataLink over RS-232/RS-485, Thermo King DSR/DSR2 over serial) or J1939 over CAN, both of which are mechanically simpler than building a PLC coupling board and better-documented at the protocol level. The firmware's `Serial1` channel and `drainReeferUart()` decode function are interface-agnostic: any 9600-baud serial source whose decode produces `setpoint_f` and `actual_f` values into `g_sensors` will exercise the rest of the alert/summary pipeline unchanged. See [§10 Limitations](#10-limitations-and-next-steps) for the side-by-side comparison.
-
-
-
-When a J2497 custom board is built, its connections to the Notecarrier CX are:
-
-- **+3V3 (out)** → J2497 custom board logic VCC (IT700 operates at 3.3 V logic). This places the J2497 board's active-mode current on the Notecarrier +3V3 rail and **on the Mojo +VBAT trace**; include it in the bench power budget accordingly.
-- **GND** → J2497 board logic GND. Keep signal ground separate from the trailer chassis ground stud.
-- **RX (Serial1)** → UART TX of the J2497 custom board (the IT700 host interface, 9600 baud, 8N1, match the protocol-stack or modem-IC datasheet). Without an application-layer stack, raw PLC modem bytes appear on this line, not decoded reefer frames. Level-shift to 3.3 V if the board's logic level is 5 V.
-- **TX (Serial1)** → UART RX of the J2497 custom board (for polling commands issued by the host to the modem).
-- **J560 pin 6 coupling:** The IT700's modulator/demodulator output couples to the J560 pin 6 (AUX) bus through the Bourns PT60234PEL coupling transformer. This is an AC-coupled signal path: the transformer primary connects to the 12 V bus for PLC signal injection/detection but draws negligible DC from that rail. Keep the transformer primary side and the J560 bus-facing circuitry isolated from the Notecarrier's +3V3 and GND rails. Consult the Bourns PT60234PEL datasheet and the Microchip IT700 PLC Design Guide for schematic, PCB layout, and bus-protection requirements; do not attempt to breadboard this signal path.
-
-**Interface summary (for when the board is built):**
-
-| Signal | Direction | Notecarrier CX pin | J2497 board pin | Notes |
-|---|---|---|---|---|
-| Logic VCC | → board | +3V3 | VCC (IT700) | ~2–15 mA active (bench-measure) |
-| Logic GND | common | GND | GND | Signal ground; isolated from chassis |
-| UART RX data | ← board | RX (Serial1) | TX (IT700 host) | 9600 baud 8N1; level-shift if 5 V |
-| UART TX cmd | → board | TX (Serial1) | RX (IT700 host) | 9600 baud 8N1 |
-| PLC bus | ← J560 pin 6 | — | Coupling xfmr | AC-coupled via PT60234PEL |
-
-### Future Integration: TPMS Receiver Path
-
-
-
-**This subsection documents future integration work, not the current reference build.** The SoftwareSerial channel on D5/D6 is compiled into the firmware as a placeholder for a TPMS receiver. No gateway should be purchased or connected until a specific vendor has been selected and the vendor's protocol decode library replaces the stub parser in `trailer_sensors.cpp`. With no gateway connected, all four TPMS positions report −9999 in every summary Note.
-
-
-
-When a TPMS OEM gateway (e.g., PressurePro CORE OEM Module) is integrated, its connections to the Notecarrier CX are:
-
-- **D6** → TPMS gateway UART TX output (SoftwareSerial RX on the host side). 9600 baud, 8N1 typical; match the gateway's datasheet. Level-shift to 3.3 V logic if the gateway operates at 5 V.
-- **D5** → TPMS gateway UART RX input (SoftwareSerial TX on the host side; used for any poll commands the host sends to the gateway, if required by the vendor protocol).
-- **12 V supply rail** → TPMS gateway VCC (the PressurePro CORE OEM module operates at 12 V DC; verify with the chosen module's datasheet). Power from the same fused 12 V rail as the DC-DC converter VIN, not through the Notecarrier's +3V3 or +5V pins. TPMS gateway GND → enclosure ground bus (shared with thermistor GND; keep separate from the trailer chassis ground stud to prevent UART noise ingress).
-
-**Production firmware change required:** replace `drainTpmsUart()` in `trailer_sensors.cpp` with the vendor's message decode library. The POC stub parses a generic 6-byte frame format that no commercial gateway produces; connecting a real gateway without this replacement will yield no valid data.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/connected_trailer_platform/connected_trailer_platform.ino` as `PRODUCT_UID`.
-2. **Provision the Notecard.** Power the unit; on first cellular or satellite session the Notecard associates with your project automatically. The device appears in Notehub under the Notecard's DeviceUID, ready for fleet assignment.
-3. **Create fleets by customer or cargo class.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. The natural unit here is one fleet per OEM customer or cargo type — frozen-goods trailers and produce trailers operate with different reefer temperature bands, and fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) encode those differences without separate firmware builds. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can automatically assign trailers to fleets based on device attributes set at provisioning time.
-4. **Set environment variables.** All variables are optional; firmware compile-time defaults apply until an override is received from Notehub. Overrides are delivered to the device on the next inbound sync without a firmware reflash.
-
- **To set variables in Notehub:** Navigate to **Fleets** → select your fleet → **Environment** tab. Add each variable as a key-value pair (e.g., key `sample_interval_sec`, value `300`). Changes are pushed to assigned devices on the next inbound sync.
-
- **Active on this build (fully-implemented sensor paths):**
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `door_open_transit_sec` | `300` | Door-open duration (seconds) while in transit above which `door_open_transit` fires — cargo security or seal-integrity event. |
- | `sample_interval_sec` | `300` | Seconds between sensor sample cycles. Increase to 900 or more during long DC dwells to reduce host activity. |
- | `summary_interval_min` | `60` | Minutes between templated window-aggregate summary Notes sent to `trailer_summary.qo`. Each Note spans all sample cycles within the window. |
- | `outbound_transit_min` | `60` | Notehub outbound sync cadence (minutes) while the trailer is in transit. |
- | `outbound_parked_min` | `240` | Notehub outbound sync cadence (minutes) during DC dwells and parked periods. |
- | `alert_cooldown_sec` | `1800` | Minimum seconds between successive same-type alerts (30-minute default prevents alert floods during a prolonged excursion). |
-
- **Integration stub variables, not active against real trailer hardware.** These thresholds are compiled in and will take effect once the corresponding sensor path is implemented with a real J2497 modem and TPMS gateway. On a build without those hardware components, the associated sensor fields remain at −9999 and no reefer or TPMS alerts fire.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `reefer_max_f` | `40.0` | Reefer actual temperature (°F) above which `reefer_temp_high` fires — cargo at risk. Evaluates against J2497 stub data only until the real J2497 path is implemented. |
- | `reefer_min_f` | `28.0` | Reefer actual temperature (°F) below which `reefer_temp_low` fires — freeze risk for temperature-sensitive loads. Evaluates against J2497 stub data only. |
- | `tpms_min_psi` | `95.0` | Tire pressure (PSI) below which `tpms_pressure_low` fires — blowout precursor. Evaluates against TPMS stub data only until the vendor decode library is implemented. |
- | `tpms_max_psi` | `130.0` | Tire pressure (PSI) above which `tpms_pressure_high` fires — over-inflation or thermal runaway. Evaluates against TPMS stub data only. |
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `trailer_alert.qo` (low-volume, real-time delivery to the OEM's fleet management system or on-call endpoint) and a second for `trailer_summary.qo` (higher-volume, batched delivery to a cold-chain compliance archive or time-series store). Separating the Notefiles at the source means each can be fanned out to a different destination at a different urgency without any filter logic in the route.
-
-## 7. Firmware Design
-
-The firmware spans three files, splitting orchestration, shared types, and sensor helpers so each concern has a clear home:
-
-| File | Role |
-|---|---|
-| [`connected_trailer_platform.ino`](firmware/connected_trailer_platform/connected_trailer_platform.ino) | Main sketch: `setup()` runs every wakeup (state restore → env-var fetch → UART drain window → sample cycle); `loop()` serializes state and sleeps via `NotePayloadSaveAndSleep()`. Also contains Notecard configuration, trailer state machine, alert evaluation, and summary-Note assembly. |
-| [`trailer_sensors.h`](firmware/connected_trailer_platform/trailer_sensors.h) | Shared header: pin assignments, protocol constants, compile-time defaults, `Config` / `Sensors` / `TempAccum` / `PersistState` struct definitions, and `extern` declarations for all globals and helper function prototypes. Included by both translation units. |
-| [`trailer_sensors.cpp`](firmware/connected_trailer_platform/trailer_sensors.cpp) | Sensor helpers: door-edge ISR and `setupDoorInterrupt()`, `drainReeferUart()`, `drainTpmsUart()`, `updateReeferMissCount()`, `readThermistors()` (16-sample β-equation ADC), and `accumulateSampleStats()`. |
-
-Dependencies:
-- Arduino core for STM32 ([`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32)).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (the `note-arduino` library). Install via the Arduino Library Manager — search for `Blues Wireless Notecard` and install the latest version, or via CLI: `arduino-cli lib install "Blues Wireless Notecard"`. See the [note-arduino releases page](https://github.com/blues/note-arduino/releases) for any update before building.
-- `SoftwareSerial` (included in the STM32 Arduino core) for the TPMS receiver on D5/D6.
-
-### Modules
-
-| Responsibility | Function |
-|---|---|
-| State serialization to Notecard + host power-off via ATTN | `NotePayloadSaveAndSleep()` (Blues library) in `loop()` |
-| State restore from Notecard sleep payload on wakeup | `NotePayloadRetrieveAfterSleep()` (Blues library) in `setup()` |
-| Notecard configuration (`hub.set`, template, GPS mode) — fresh boot only | `notecardInit()`, `defineTemplate()` |
-| Sync cadence switching on state transition | `applyHubSetIfChanged()` |
-| Environment variable fetch and application — every wake | `fetchEnvOverrides()` |
-| Post-wakeup UART drain window (WAKE_UART_DRAIN_MS) | inline in `setup()`, calls `drainReeferUart()` / `drainTpmsUart()` |
-| J2497 UART drain, frame latch, j2497Commissioned gate | `drainReeferUart()` |
-| Reefer miss-counter assessment (j2497Commissioned gated) | `updateReeferMissCount()` |
-| Thermistor ADC read and β-equation conversion | `readThermistors()`, `adcToF()` |
-| TPMS UART drain, pressure latch, tpmsPsiLast persistence | `drainTpmsUart()` |
-| Per-sample window accumulation (temps, door) | `accumulateSampleStats()` |
-| Trailer state machine (parked / loading / in transit) | `updateTrailerState()` |
-| Alert threshold evaluation and cooldown tracking | `evaluateAlerts()`, `sendAlert()` |
-| Windowed summary Note (min/max/mean aggregates) + epoch window advance | `sendSummary()` |
-
-### Sensor reading strategy
-
-- **Reefer temperature (J2497 UART).** `drainReeferUart()` runs during the WAKE_UART_DRAIN_MS window at wakeup and between blocking Notecard I²C calls within the sample cycle, scanning `Serial1` for the two-byte header (0xAA 0x55) and verifying the XOR checksum before latching the setpoint and actual-temperature fields (signed 16-bit, 0.1 °F resolution) into `g_sensors`. At each wakeup, `updateReeferMissCount()` checks whether a valid frame was latched since the last wakeup; three consecutive wakeup cycles without a valid frame trigger `reefer_sensor_loss`, **but only after `g_ps.j2497Commissioned` is true** (set on first accepted frame). A build with no J2497 modem connected never sets this flag and never fires `reefer_sensor_loss`. Note that this path models a simplified POC frame format: a production J2497 data path requires a full application-layer protocol stack and reefer-OEM message mapping before actual reefer data can be decoded; see [§10 Limitations](#10-limitations-and-next-steps).
-- **Thermistors.** 16-sample averaged 12-bit ADC reads on A0 and A1, converted to resistance through the 10 kΩ divider equation, then to °F via the Steinhart-Hart β approximation (β = 3950 K). Averaging 16 samples suppresses short-term ADC noise and fluctuations from refrigeration discharge-air currents near the probes.
-- **TPMS.** `drainTpmsUart()` runs during the WAKE_UART_DRAIN_MS window and between Notecard I²C calls within the sample cycle. `SoftwareSerial` requires an active CPU; it does not receive during the host-off sleep interval. The most recent valid pressure per position is latched in both `g_sensors.tpmsPsi[]` and `g_ps.tpmsPsiLast[]`; the persisted copy survives the sleep interval and is restored into `g_sensors.tpmsPsi[]` at wakeup so `sendSummary()` always reports the correct last-known value. Each `sendSummary()` call reports the last-known pressure for all four positions plus a per-position `tpms_N_age` field (summary windows elapsed since the last valid frame for that position). A position that has not reported for `TPMS_STALE_COUNT` (2) or more consecutive summary windows emits the −9999 sentinel for its pressure field.
-- **Door state.** `digitalRead(PIN_DOOR)` with `INPUT_PULLUP`. When the door is **closed**, the magnet holds the N.C. contact open; the pull-up drives the pin **HIGH** (`doorOpen = false`). When the door **opens**, the magnet moves away and the N.C. contact closes to GND, pulling the pin **LOW** (`doorOpen = true`). The firmware maps `digitalRead(PIN_DOOR) == LOW` → `doorOpen = true`. A hardware interrupt (`CHANGE`) is attached once in `setup()` and latches the last stable pin state so rapid transitions within one sample interval resolve correctly at sample time. Door open-duration (`door_open_min`) and event count (`door_event_count`) are computed at `sample_interval_sec` granularity: only transitions observed at consecutive sample-cycle boundaries are captured; an open/close event that begins and ends entirely within a single interval is not counted.
-
-**UART acquisition window.** Both UART channels are drained during the `WAKE_UART_DRAIN_MS` (250 milliseconds) window at the start of each wakeup, and again between each major Notecard I²C call within the sample cycle (`env.get`, `card.time`, `card.location`, `note.add`). At 9600 baud the STM32L433's 64-byte hardware UART ring buffer fills in roughly 67 milliseconds, shorter than some Notecard transaction latencies under radio load. The inter-call drains mitigate this by clearing the hardware buffer between transactions. Even with interleaved draining, frame loss during a multi-alert burst (up to six consecutive `note.add` calls) or a slow Notecard response remains possible. The most recent valid frame from each channel is latched in `g_sensors` (and in `g_ps.tpmsPsiLast[]` for TPMS); the sample cycle reads these latched values. For the TPMS channel, SoftwareSerial's bit-banging requires the CPU to be active; for a high-rate TPMS gateway or for production deployments where frame loss is unacceptable, move TPMS to a second hardware UART if one is available on the target board.
-
-### Event payload design
-
-`trailer_summary.qo` is [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) — Notes encode as fixed-length binary records rather than free-form JSON, cutting wire size roughly 3–5× relative to unformatted JSON. Over 10 years of hourly summaries on a multi-trailer fleet, the savings against the included 500 MB data plan are material. Each summary Note carries **window-aggregate statistics** for the completed `summary_interval_min` interval rather than a point-in-time snapshot: min/max/mean actual temperature for the reefer and both cargo-air probes (the key cold-chain record), cumulative door-open minutes and distinct open event count, last-known tire pressure per TPMS position plus a per-position age field (summary windows since last frame received), and a sample-count confirming how many cycles contributed to the window. `trailer_alert.qo` is untemplated so the payload can carry the full current sensor snapshot without maintaining a per-alert template registry. Every alert Note carries the same set of fields regardless of alert type: alert name, reefer set and actual temperature, both air-probe temperatures, door state, and all four TPMS pressure readings (−9999 for positions with no current data).
-
-**Prototype output: what a bench build actually produces.** Without a real J2497 source or TPMS gateway connected, reefer pressure fields and all `tpms_N_psi` fields read −9999 in every Note. The `tpms_N_age` fields start at 0 on fresh boot and advance by 1 on each summary boundary at which no data was received for that position: age 1 after the first window, 2 after the second, and so on. The firmware's `sendSummary()` increments the stale counter for every position not marked `tpmsSeenThisWindow` before resetting the window. A TPMS position reports age 0 only when a valid frame for that position was received within the summary window just closed. The real-sensor fields (`air_t*`, `door_*`, `trailer_state`, `sample_count`) are populated from hardware on every cycle.
-
-Example `trailer_summary.qo` body, bench build with real cargo-air thermistors and door sensor, no J2497 or TPMS hardware (12-sample window):
-```json
-{
- "reefer_set_f": -9999.0,
- "reefer_min_f": -9999.0,
- "reefer_max_f": -9999.0,
- "reefer_mean_f": -9999.0,
- "air_t1_min_f": 35.6,
- "air_t1_max_f": 37.2,
- "air_t1_mean_f": 36.3,
- "air_t2_min_f": 35.1,
- "air_t2_max_f": 36.8,
- "air_t2_mean_f": 35.9,
- "door_open_min": 0.0,
- "door_event_count": 0,
- "tpms_0_psi": -9999.0,
- "tpms_1_psi": -9999.0,
- "tpms_2_psi": -9999.0,
- "tpms_3_psi": -9999.0,
- "tpms_0_age": 1,
- "tpms_1_age": 1,
- "tpms_2_age": 1,
- "tpms_3_age": 1,
- "trailer_state": 2,
- "sample_count": 12
-}
-```
-
-*`tpms_N_age` starts at 0 on boot and advances by 1 at each summary boundary where no data was received: age 1 above is correct for the first window on a build with no TPMS gateway. On the second window with no data, ages read 2 (= `TPMS_STALE_COUNT`), after which the pressure field is already at −9999 from initialization and the age counter continues climbing as a diagnostic. A position that reported data in the just-closed window always shows age 0 regardless of prior history. See the full-platform example below.*
-
-**Alert payload, example `trailer_alert.qo`** (fired immediately when `door_open_transit` threshold is exceeded; JSON is untemplated so it carries the full sensor snapshot, not a binary record):
-```json
-{
- "alert": "door_open_transit",
- "reefer_actual_f": -9999.0,
- "reefer_set_f": -9999.0,
- "air_t1_f": 36.8,
- "air_t2_f": 36.2,
- "door_open": true,
- "tpms_0_psi": -9999.0,
- "tpms_1_psi": -9999.0,
- "tpms_2_psi": -9999.0,
- "tpms_3_psi": -9999.0
-}
-```
-The Notecard tags every Note with the last known GPS position (`_location`) and a device-scoped timestamp (`_time`) set from Notehub's clock on inbound sync. Alerts fire with `sync: true`, triggering an immediate radio session; delivery latency is seconds over cellular, a few minutes over Skylo NTN satellite.
-
-**Full-platform output: expected shape once J2497 and TPMS integrations are complete.** When a real J2497 source and TPMS gateway are connected and the vendor decode libraries are in place, the same template produces a populated record. Fields marked *(stub)* below are −9999 until the corresponding integration is complete.
-
-Example `trailer_summary.qo` body, full platform, frozen-food transit (12-sample window):
-```json
-{
- "reefer_set_f": 34.0, // stub until J2497 path is implemented
- "reefer_min_f": 34.8, // stub
- "reefer_max_f": 36.1, // stub
- "reefer_mean_f": 35.2, // stub
- "air_t1_min_f": 35.6,
- "air_t1_max_f": 37.2,
- "air_t1_mean_f": 36.3,
- "air_t2_min_f": 35.1,
- "air_t2_max_f": 36.8,
- "air_t2_mean_f": 35.9,
- "door_open_min": 0.0,
- "door_event_count": 0,
- "tpms_0_psi": 105.0, // stub until TPMS vendor library is implemented
- "tpms_1_psi": 103.0, // stub
- "tpms_2_psi": 108.0, // stub
- "tpms_3_psi": 107.0, // stub
- "tpms_0_age": 0,
- "tpms_1_age": 0,
- "tpms_2_age": 0,
- "tpms_3_age": 0,
- "trailer_state": 2,
- "sample_count": 12
-}
-```
-
-Example `trailer_alert.qo` body (with `sync:true`, immediately transmitted), `door_open_transit` from a real door event:
-```json
-{
- "alert": "door_open_transit",
- "reefer_actual_f": -9999.0,
- "reefer_set_f": -9999.0,
- "air_t1_f": 36.3,
- "air_t2_f": 35.9,
- "door_open": true,
- "tpms_0_psi": -9999.0,
- "tpms_1_psi": -9999.0,
- "tpms_2_psi": -9999.0,
- "tpms_3_psi": -9999.0
-}
-```
-
-### Low-power strategy
-
-**Dwell-capable host sleep via `card.attn`.** After each sample cycle `loop()` serializes `PersistState` into the Notecard via `NotePayloadSaveAndSleep()`, which issues `card.attn` with `mode=sleep` and `sleepSeconds=sampleIntervalSec`, then signals the Notecarrier CX to cut host power via the ATTN line. The host draws **zero current** during the sleep interval — only the Notecard's own radio-idle floor remains on the +VBAT rail. The Notecard pulses ATTN after `sampleIntervalSec` seconds; `setup()` re-runs on the next wakeup, restores `PersistState` from the Notecard's storage notefile via `NotePayloadRetrieveAfterSleep()`, and runs one complete sample cycle before sleeping again.
-
-The key power budget figures:
-
-| Phase | +VBAT draw | Duration |
-|---|---|---|
-| Host asleep (Notecard idle between sessions) | ~8–18 µA (Blues-published; see [Blues low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)) | Majority of the 24-hour day |
-| Host awake — sample cycle + Notecard I²C calls | Bench-measure with Mojo — dominated by Cygnet STM32 active current; no Blues-published spec exists | ~1–3 seconds per `sampleIntervalSec` cycle |
-| Notecard cellular session | No Blues-published specification for NOTE-NBGLWX session current — bench-measure with Mojo | Per outbound cadence |
-| Notecard Skylo NTN satellite session | No Blues-published specification for NOTE-NBGLWX NTN session current — bench-measure with Mojo | Only when cellular unavailable |
-
-DC-dwell battery sizing should be based on bench-measured Mojo data, not on component-level estimates. See [§9 Validation and Testing](#9-validation-and-testing) for the measurement procedure.
-
-**UART acquisition during host-off sleep.** Both UART peripherals lose power when the host is off. On wakeup, `setup()` re-initializes `Serial1` (J2497) and `tpmsSerial` (TPMS) and then drains both channels for `WAKE_UART_DRAIN_MS` (250 milliseconds) before the sample cycle. At 9600 baud, 250 milliseconds accommodates ~3 complete J2497 frames. The host then stays awake for the duration of the sample cycle (typically 1–5 seconds including Notecard I²C calls), during which both channels are drained between each blocking call. For the J2497 path, frames arriving during a Notecard I²C call are captured in the STM32's 64-byte hardware UART ring buffer; loss is possible only if a single Notecard call takes longer than ~67 milliseconds (full buffer at 9600 baud). For the TPMS path, `SoftwareSerial` requires an active CPU; it captures only bytes that arrive while the host is awake. TPMS gateways that buffer the most recent frame and re-transmit periodically are the most compatible; gateways that transmit only on pressure change may miss wakeup windows during stable-pressure dwell periods.
-
-**TPMS pressure continuity across wakes.** `g_ps.tpmsPsiLast[]` in `PersistState` carries the most recent valid pressure per position across the sleep interval. `setup()` restores these values into `g_sensors.tpmsPsi[]` at wakeup before the drain window runs, so `sendSummary()` always reports the correct last-known pressure even if no fresh frame arrived in the current wake's drain window. The stale counter (`tpmsStaleCounts[]`) continues to advance per summary window, correctly aging out silent positions to the −9999 sentinel.
-
-**Fallback — no ATTN host-power control.** If ATTN is not wired for host power control (bench commissioning without the ATTN connection), `NotePayloadSaveAndSleep()` returns without cutting host power and `loop()` falls through to a `delay()` that mimics the sample cadence. The host draws continuous active current (~5–15 mA, bench-measure) in this mode. The USB debug output indicates which path is active: look for `[sleep] ATTN not cutting host power — using delay fallback` on the serial console.
-
-Notecard for Skylo manages its own radio sleep independently: in [`periodic`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) mode it idles at ~8–18 µA between sessions, then wakes briefly for a cellular or satellite burst on the configured outbound cadence. The sync cadence switches automatically on every trailer state transition: `outbound_transit_min` (default 60 minutes) while moving, `outbound_parked_min` (default 240 minutes) during DC dwells. The host re-issues `hub.set` each time the cadence changes so the Notecard tracks the current schedule exactly.
-
-Operators can extend `sample_interval_sec` via environment variable during long-dwell parked periods (for example, from 300 seconds to 900 seconds) to reduce the host's active-duty cycle when cargo temperature is stable.
-
-### Retry and error handling
-
-- The first Notecard transaction uses `sendRequestWithRetry(req, 10)` to handle the cold-boot I²C race condition documented in the `note-arduino` library.
-- `fetchEnvOverrides()` performs a full `env.get` on every sample cycle and resets `g_cfg` to compiled defaults first, so any variable removed from Notehub reverts to its compiled default in the same cycle that receives the inbound sync. Sync cadence changes are applied immediately via `applyHubSetIfChanged()`, which guards the I²C round-trip behind an equality check and only updates local state after the Notecard confirms the new setting.
-- The J2497 parse failure counter fires the `reefer_sensor_loss` alert after three consecutive wakeup cycles without a valid frame, but **only if `g_ps.j2497Commissioned` is true** (the flag is set on first accepted frame). On a build with no J2497 modem, this flag is never set and the alert never fires, eliminating spurious loss alerts during bench validation. The TPMS position-silence tracker ages stale positions to −9999 in summaries after two missed windows; it does **not** fire a separate alert. A silent TPMS sensor is reflected as missing data in `trailer_summary.qo`, not as an immediate `trailer_alert.qo` event.
-- Alert de-duplication via `alert_cooldown_sec` (default 30 minutes per alert type) prevents a slowly-drifting temperature sensor from paging the fleet manager on every 5-minute sample cycle.
-- All `requestAndResponse()` calls check both a `NULL` return value and the `err` field on the response object before trusting the data.
-
-### Key code snippet 1: host sleep and state persistence across wakes
-
-`loop()` serializes `PersistState` into the Notecard and calls `NotePayloadSaveAndSleep()`, which issues `card.attn` and signals the Notecarrier CX to cut host power. `setup()` restores the state on every wakeup. All inter-sample context — accumulators, alert cooldowns, door state, TPMS pressures, summary window epoch — survives the host-off interval.
-
-```cpp
-// In loop() — runs once per wakeup, after the sample cycle completes:
-void loop() {
- drainReeferUart();
- drainTpmsUart(); // final drain before power cut
-
- NotePayloadDesc payload = {};
- NotePayloadAddSegment(&payload, STATE_SEG_ID, &g_ps, sizeof(g_ps));
- NotePayloadSaveAndSleep(&payload, g_cfg.sampleIntervalSec, NULL);
-
- // Reached only if ATTN is not cutting host power (bench fallback):
- delay(g_cfg.sampleIntervalSec * 1000UL);
-}
-
-// In setup() — runs on every wakeup:
-void setup() {
- // ... hardware init (Serial1, tpmsSerial, pinMode, notecard.begin) ...
-
- NotePayloadDesc payload = {};
- bool restored = NotePayloadRetrieveAfterSleep(&payload);
- if (restored) {
- restored &= NotePayloadGetSegment(&payload, STATE_SEG_ID,
- &g_ps, sizeof(g_ps));
- NotePayloadFree(&payload);
- }
- if (!restored) {
- memset(&g_ps, 0, sizeof(g_ps));
- // ... initialize fields, call notecardInit() ...
- }
-
- fetchEnvOverrides(); // always re-fetch OTA config changes
- setupDoorInterrupt(); // re-attach on every wakeup
-
- // Restore last-known TPMS pressures so sendSummary() has correct values
- // for positions that did not transmit in this drain window.
- for (int i = 0; i < NUM_TPMS_POS; i++)
- g_sensors.tpmsPsi[i] = g_ps.tpmsPsiLast[i];
-
- // Brief drain window to catch frames arriving after UART re-init:
- uint32_t drainUntil = millis() + WAKE_UART_DRAIN_MS;
- while (millis() < drainUntil) { drainReeferUart(); drainTpmsUart(); }
-
- uint32_t nowEpoch = notecardEpoch();
- if (g_ps.summaryWindowStartEpoch == 0 && nowEpoch != 0)
- g_ps.summaryWindowStartEpoch = nowEpoch;
-
- runSampleCycle(nowEpoch);
-}
-```
-
-### Key code snippet 2: hub.set cadence switching on state transition
-
-When the trailer transitions between parked and in-transit, the host re-issues `hub.set` with the new outbound cadence. The guard clause skips the I²C round trip when the cadence hasn't changed.
-
-```cpp
-static bool applyHubSetIfChanged(uint32_t newOutboundMin) {
- if (newOutboundMin == g_ps.currentOutboundMin) return true;
- J *req = notecard.newRequest("hub.set");
- if (!req) return false;
- JAddStringToObject(req, "mode", "periodic");
- JAddNumberToObject(req, "outbound", (int)newOutboundMin);
- JAddNumberToObject(req, "inbound", (int)newOutboundMin * 2);
- J *rsp = notecard.requestAndResponse(req);
- if (!rsp) return false;
- bool ok = !notecard.responseError(rsp);
- notecard.deleteResponse(rsp);
- if (ok) g_ps.currentOutboundMin = newOutboundMin; // update only on success
- return ok;
-}
-```
-
-### Key code snippet 3: immediate-sync temperature alert
-
-`sync:true` tells the Notecard to skip the next scheduled outbound window and open a radio session immediately. Over cellular this typically delivers the alert within seconds; over Skylo NTN satellite it may take a few minutes for the first acquisition, but the alert is queued and delivered even if cellular coverage is unavailable.
-
-```cpp
-static void sendAlert(uint8_t idx, uint32_t nowEpoch) {
- if (!alertCooldownOk(idx, nowEpoch)) return;
- J *req = notecard.newRequest("note.add");
- if (!req) return;
- JAddStringToObject(req, "file", NOTE_ALERT);
- JAddBoolToObject(req, "sync", true);
- J *body = JAddObjectToObject(req, "body");
- JAddStringToObject(body, "alert", kAlertNames[idx]);
- JAddNumberToObject(body, "reefer_actual_f", g_sensors.reeferActualF);
- JAddNumberToObject(body, "reefer_set_f", g_sensors.reeferSetF);
- JAddNumberToObject(body, "air_t1_f", g_sensors.airT1F);
- JAddNumberToObject(body, "air_t2_f", g_sensors.airT2F);
- JAddBoolToObject(body, "door_open", g_sensors.doorOpen);
- JAddNumberToObject(body, "tpms_0_psi", g_sensors.tpmsPsi[0]);
- JAddNumberToObject(body, "tpms_1_psi", g_sensors.tpmsPsi[1]);
- JAddNumberToObject(body, "tpms_2_psi", g_sensors.tpmsPsi[2]);
- JAddNumberToObject(body, "tpms_3_psi", g_sensors.tpmsPsi[3]);
- J *rsp = notecard.requestAndResponse(req);
- if (!rsp) return;
- bool ok = !notecard.responseError(rsp);
- notecard.deleteResponse(rsp);
- // Stamp both clocks only after confirmed delivery so a failed send does
- // not silently suppress the alert for the full cooldown window.
- if (ok) {
- if (nowEpoch != 0) g_ps.lastAlertEpoch[idx] = nowEpoch; // epoch-based: persists across wakes
- g_lastAlertMs[idx] = millis(); // millis fallback: within this session only
- }
-}
-```
-
-### Key code snippet 4: Note template for bandwidth efficiency
-
-Templated Notes store each summary as a fixed-width binary record: `14.1` encodes a 4-byte float, `12` encodes a 2-byte signed integer. The 22-field template contains 15 float fields (4 bytes each) and 7 int16 fields (2 bytes each), totaling 74 bytes of user payload before any per-record Notecard overhead, substantially more compact than the several hundred bytes that equivalent free-form JSON would require for the same field set. The savings compound meaningfully over a 10-year, multi-trailer deployment on the included 500 MB data plan.
-
-```cpp
-static bool defineTemplate() {
- J *req = notecard.newRequest("note.template");
- if (!req) return false;
- JAddStringToObject(req, "file", NOTE_SUMMARY);
- JAddNumberToObject(req, "port", TEMPLATE_PORT);
- J *body = JAddObjectToObject(req, "body");
- // Reefer: last setpoint + window min/max/mean of actual temp
- JAddNumberToObject(body, "reefer_set_f", 14.1);
- JAddNumberToObject(body, "reefer_min_f", 14.1);
- JAddNumberToObject(body, "reefer_max_f", 14.1);
- JAddNumberToObject(body, "reefer_mean_f", 14.1);
- // Cargo-air: window min/max/mean for each probe
- JAddNumberToObject(body, "air_t1_min_f", 14.1);
- JAddNumberToObject(body, "air_t1_max_f", 14.1);
- JAddNumberToObject(body, "air_t1_mean_f", 14.1);
- JAddNumberToObject(body, "air_t2_min_f", 14.1);
- JAddNumberToObject(body, "air_t2_max_f", 14.1);
- JAddNumberToObject(body, "air_t2_mean_f", 14.1);
- // Door: cumulative open minutes + distinct event count
- JAddNumberToObject(body, "door_open_min", 14.1);
- JAddNumberToObject(body, "door_event_count", 12);
- // TPMS: last-known pressure + windows-since-last-frame per position
- JAddNumberToObject(body, "tpms_0_psi", 14.1);
- JAddNumberToObject(body, "tpms_1_psi", 14.1);
- JAddNumberToObject(body, "tpms_2_psi", 14.1);
- JAddNumberToObject(body, "tpms_3_psi", 14.1);
- JAddNumberToObject(body, "tpms_0_age", 12);
- JAddNumberToObject(body, "tpms_1_age", 12);
- JAddNumberToObject(body, "tpms_2_age", 12);
- JAddNumberToObject(body, "tpms_3_age", 12);
- JAddNumberToObject(body, "trailer_state", 12);
- JAddNumberToObject(body, "sample_count", 12);
- J *rsp = notecard.requestAndResponse(req);
- if (!rsp) return false;
- bool ok = !notecard.responseError(rsp);
- notecard.deleteResponse(rsp);
- return ok;
-}
-```
-
-### Key code snippet 5: GPS-based motion detection
-
-Rather than requiring a dedicated speed field from the GNSS module, the firmware compares consecutive GPS positions. A squared position delta greater than 4.0×10⁻⁷ degree² — approximately 50 m per axis at mid-latitudes when both axes contribute equally — between two 5-minute samples classifies the trailer as moving. The check also requires a valid prior fix (`hasPrev`), so the first GPS acquisition after power-up never falsely triggers a state change. When GPS fix is unavailable (GNSS gap, error response, or first boot with no prior position), the previous trailer state is preserved unchanged; reclassifying on door state alone would incorrectly demote an in-transit trailer during a temporary outage and would suppress `door_open_transit` alerts.
-
-```cpp
-static void updateTrailerState(uint32_t nowEpoch) {
- (void)nowEpoch;
- J *rsp = notecard.requestAndResponse(notecard.newRequest("card.location"));
- if (!rsp) return;
-
- const char *errStr = JGetString(rsp, "err");
- bool hasErr = (errStr && errStr[0] != '\0');
-
- float lat = (float)JGetNumber(rsp, "lat");
- float lon = (float)JGetNumber(rsp, "lon");
- notecard.deleteResponse(rsp);
-
- bool validFix = !hasErr && (lat != 0.0f || lon != 0.0f);
- bool hasPrev = (g_ps.lastLat != 0.0f || g_ps.lastLon != 0.0f);
-
- if (validFix && hasPrev) {
- float dL = lat - g_ps.lastLat, dO = lon - g_ps.lastLon;
- bool moving = (dL * dL + dO * dO) > 4.0e-7f; // ≈ (50 m)² threshold
- g_ps.trailerState = moving ? STATE_IN_TRANSIT :
- g_sensors.doorOpen ? STATE_LOADING : STATE_PARKED;
- }
- // No valid fix or no prior fix: preserve current state — do not reclassify
- // on door state alone. Doing so would demote an in-transit trailer during a
- // GNSS gap and suppress door_open_transit alerts while the trailer is moving.
- if (validFix) { g_ps.lastLat = lat; g_ps.lastLon = lon; }
-
- applyHubSetIfChanged(
- g_ps.trailerState == STATE_IN_TRANSIT ? g_cfg.outboundTransitMin
- : g_cfg.outboundParkedMin);
-}
-```
-
-## 8. Data Flow
-
-
-
-The Notecard's `card.attn` wakes the host every `sample_interval_sec` (default 300 seconds / 5 minutes). On each wakeup the host restores `PersistState`, drains both UART channels for 250 milliseconds, reads all sensor inputs, accumulates window statistics, evaluates threshold rules, and may emit one or more immediate alerts before sleeping again. Every `summary_interval_min` (default 60 minutes) the host emits one templated summary Note carrying aggregated statistics for the completed window, not a point-in-time snapshot.
-
-**Collected on each wake cycle:**
-- Front and rear cargo-air temperature (°F) from two NTC thermistors: **fully implemented**
-- Rear door state (open / closed) from reed switch: **fully implemented**
-- Trailer state (parked / loading / in transit) derived from Notecard GNSS position delta: **fully implemented**
-- Reefer setpoint and actual temperature (°F) parsed from a simplified POC UART frame: **integration-point stub; requires J2497 protocol stack and reefer-OEM mapping to produce real data** (see §10)
-- Four tire-position pressures (PSI) parsed from a simplified POC UART packet: **integration-point stub; requires vendor-specific TPMS decode library** (see §10)
-
-**Transmitted:**
-- `trailer_summary.qo`: one template-encoded window-aggregate record every `summary_interval_min` (default 24 per day per trailer), carrying reefer and cargo-air min/max/mean temperatures, door-open minutes and event count, TPMS last-known pressures and per-position ages, and a sample-count. Queued in the Notecard and flushed on the configured outbound cadence.
-- `trailer_alert.qo`: emitted only on a threshold trip, with `sync:true` for immediate delivery, subject to the 30-minute per-type cooldown window.
-
-**GPS.** `card.location.mode` is set to `periodic` with a 300-second interval. In `periodic` mode the Notecard only powers up the GNSS module if its onboard accelerometer has detected motion since the last fix attempt, so the GPS radio stays off during stationary DC dwells and turns on only after the trailer moves. See [Sampling GPS Readings with Periodic Mode](https://dev.blues.io/notecard/notecard-walkthrough/time-and-location-requests/#sampling-gps-readings-with-periodic-mode). The Notecard automatically tags each outbound Note with the last known position.
-
-**Routed.** Notehub fans `trailer_alert.qo` to the OEM's real-time fleet management or on-call system. `trailer_summary.qo` flows to a cold-chain compliance archive or time-series data store for trend analysis and FSMA (**Food Safety Modernization Act**) recordkeeping.
-
-**Triggers for alerts:**
-
-*Fully-implemented sensor paths, fire against actual hardware:*
-- `door_open_transit`: rear door open longer than `door_open_transit_sec` while the trailer is classified in-transit; cargo security event or seal integrity concern.
-
-*Integration stub paths, compile and evaluate in firmware but fire only against simulated POC frames; not active on real trailer hardware until the corresponding integration is complete:*
-- `reefer_temp_high`: reefer actual temp above `reefer_max_f` (default 40 °F). Fires against J2497 stub data only.
-- `reefer_temp_low`: reefer actual temp below `reefer_min_f` (default 28 °F), freeze risk. Fires against J2497 stub data only.
-- `reefer_sensor_loss`: three consecutive wake cycles with no valid J2497 frame; PLC bus or reefer unit communication fault. **Gated by `j2497Commissioned`**: fires only after at least one valid J2497 frame has been accepted since last boot. A build with no J2497 modem connected never sets this flag, so no spurious loss alerts are produced on the bench.
-- `tpms_pressure_low`: any tire below `tpms_min_psi`, blowout precursor. Fires against TPMS stub data only.
-- `tpms_pressure_high`: any tire above `tpms_max_psi`, over-inflation or heat buildup. Fires against TPMS stub data only.
-
-## 9. Validation and Testing
-
-**Expected steady-state behavior, bench build with thermistors and door only (no J2497 or TPMS hardware).** On a powered build you'll see one `trailer_summary.qo` event per hour with populated `air_t*` and `door_*` fields, −9999 in all `reefer_*` and `tpms_N_psi` fields, and `tpms_N_age` values of 1 after the first summary window, 2 after the second, and continuing to climb each window (the stale counter is not capped, the pressure field is already at −9999 from initialization and the age serves as a diagnostic window-since-last-reception counter). `trailer_state` will be 0 (parked) until GPS acquires a fix and motion is detected. Zero `trailer_alert.qo` events is the correct baseline: the `reefer_sensor_loss` alert is gated behind `j2497Commissioned`, which is only set when the first valid J2497 frame is accepted; it will never fire on a build with no J2497 modem connected regardless of how many wakeup cycles pass without a frame. Only `door_open_transit` can fire against real hardware on the bench. During a door open/close event you'll see `door_open_min` > 0 and `door_event_count: 1` in the next summary Note. The `door_open` boolean field appears only in `trailer_alert.qo`, never in the templated summary Note.
-
-**Power validation with Mojo.**
-
-**Measurement point.** Splice the [Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) inline on the Notecarrier CX **+VBAT** rail. This captures Notecard for Skylo, the Cygnet STM32 host, and any 3.3 V-railed peripherals (including, when eventually built, the J2497 custom board's IT700 logic VCC, sourced from the Notecarrier +3V3). The TPMS gateway is powered directly from the 12 V trailer rail — upstream of the DC-DC converter, and **does not appear in the Mojo trace**. For a complete DC-dwell energy budget, add a second measurement at the DC-DC converter's 12 V input rail using a bench current probe or dedicated meter.
-
-**Blues-published reference figures for the NOTE-NBGLWX on +VBAT.**
-
-| State | Rail | Published figure | Source |
-|---|---|---|---|
-| Notecard idle (radio off, between syncs) | +VBAT | ~8–18 µA | **Blues-published** — Blues [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) |
-
-The Notecard idle figure is the only Blues-published power specification for the NOTE-NBGLWX on this rail. All other operating states (host-awake sample cycles, LTE-M/NB-IoT cellular sessions, and Skylo NTN satellite sessions) have no Blues-published figures for this SKU. **Measure each state on your bench with the Mojo before using any figure in a power design or a DC-dwell battery sizing calculation.** Do not use modem-IC datasheet figures as proxies for Notecard-level current; they do not account for the Notecard's own regulators, baseband, and power management overhead.
-
-| State | Measurement point | What to do |
-|---|---|---|
-| Host awake — sample cycle (ADC reads + Notecard I²C calls) | +VBAT | **Bench-measure with Mojo.** No Blues-published spec; varies with STM32 clock rate, active peripherals, and Notecard I²C latency. |
-| Notecard LTE-M/NB-IoT cellular session | +VBAT | **Bench-measure with Mojo.** No Blues-published NOTE-NBGLWX cellular session current. |
-| Notecard Skylo NTN satellite session | +VBAT | **Bench-measure with Mojo in a real outdoor satellite-coverage location.** No Blues-published NOTE-NBGLWX NTN session current; allow several minutes for initial satellite acquisition. |
-| Host asleep (ATTN cutting host power) | +VBAT | Dominated by Notecard idle: ~8–18 µA (Blues-published). This is the figure that makes DC-dwell operation viable. |
-| TPMS gateway (12 V, future integration) | 12 V rail — upstream of DC-DC, **not on Mojo** | Per vendor datasheet; confirm at bench. Does not appear in the Mojo trace. |
-| J2497 custom board logic (future integration, 3.3 V) | +VBAT via Notecarrier +3V3 — **will appear on Mojo trace** | Bench-measure when the custom board is built; do not estimate. |
-
-**What to look for on the Mojo trace.** Run the unit for 24 hours with all antennas attached. With ATTN-gated host sleep active, the Mojo trace should show a near-floor ~8–18 µA baseline (Notecard idle) with periodic brief spikes for host-awake sample cycles and larger spikes at the configured outbound cadence for radio sessions. If the baseline never drops to the µA floor, the host is not sleeping — check that `NotePayloadSaveAndSleep()` is returning and that the debug console shows no `[sleep] ATTN not cutting host power` message. To observe the Notecard's µA idle floor in isolation and confirm ATTN-gated sleep is working, power-cycle the unit and watch the first sleep interval on the Mojo trace — the host-awake spike from `setup()` should be followed by a drop to the µA floor before the next scheduled wakeup.
-
-**DC-dwell battery sizing.** Because the host draws zero current while sleeping (only the Notecard's ~8–18 µA floor remains), multi-day DC-dwell survival is achievable within a reasonable reefer battery budget. Use only bench-measured Mojo data (specifically the 24-hour average current at the Mojo measurement point) in any dwell sizing formula. Apply: `dwell_days = (battery_Ah × 1000) / (I_avg_mA × 24)`. Measure separately at the 12 V input rail to capture TPMS gateway draw and DC-DC converter quiescent loss, which do not appear in the +VBAT Mojo trace.
-
-**Threshold simulation, real sensors.** To verify that the `door_open_transit` alert path is end-to-end functional, open the rear door while the unit is in transit (or while simulating transit by setting `outbound_transit_min` to 1 and waiting for a GPS fix). Leave the door open longer than `door_open_transit_sec` (default 5 minutes). A `trailer_alert.qo` Note with `"alert": "door_open_transit"` should appear in Notehub within one cellular session-establishment window.
-
-**Threshold simulation, integration stubs.** The reefer and TPMS alert paths evaluate against the stub parsers, not real hardware. Without a device injecting POC-format frames over Serial1 or D6, `g_sensors.reeferActualF` and `g_sensors.tpmsPsi[]` remain at −9999 and no reefer or TPMS alerts fire regardless of threshold settings. To exercise those code paths on the bench: (a) set `reefer_max_f` in Notehub to a value above −9999 (any positive threshold will suffice since the stub-received values, if injected, would be real-looking numbers), then (b) connect a USB-to-serial adapter to Serial1 and transmit a valid 8-byte POC frame (`0xAA 0x55` header, big-endian signed 16-bit setpoint and actual temp in 0.1 °F units, status byte, XOR checksum of bytes 2–6) at 9600 baud. The next sample cycle that latches the frame will evaluate the threshold and fire `reefer_temp_high` if the injected actual temperature exceeds `reefer_max_f`. This exercises the firmware's threshold-evaluation and `note.add` path but does not validate any J2497 physical-layer or protocol-layer behavior.
-
-**Satellite fallback validation.** Move the powered unit — with **all antennas properly attached** — to an **outdoor location** with confirmed no LTE-M cellular coverage but a **clear, unobstructed sky view**. A rural field, open parking area, or rooftop site with verified weak/no LTE-M coverage (confirm against the carrier's coverage map before the trip) is ideal. **Do not use a basement, building interior, or enclosed metal container** — these structures block the satellite signal in addition to cellular, giving the Notecard no radio path at all; no delivery will occur. Do not disconnect a u.FL connector while the unit is powered; doing so creates an open-circuit impedance mismatch that can damage the modem front end. With all antennas connected and no cell signal available, Notecard for Skylo will exhaust its cellular registration attempts and transition automatically to the Skylo NTN satellite path. A pending summary Note should eventually appear in Notehub — allow several minutes for initial satellite acquisition; subsequent sessions are faster once the modem has acquired the satellite ephemeris. **To confirm satellite delivery:** in Notehub navigate to **Events**, filter on your device, and open the `_session.qo` system Note created for the same session as your delivered event (see the [system notefiles reference](https://dev.blues.io/api-reference/system-notefiles/)). For a **cellular session**, the `_session.qo` body includes populated `tower_id`, `tower_lat`, and `tower_lon` fields identifying the cell tower that carried the session. For a **Skylo NTN satellite session**, those tower fields are absent — no cell tower was involved. This tower-field presence/absence is the reliable, documented indicator of transport type.
-
-## 10. Limitations and Next Steps
-
-This reference platform delivers the three sensor paths a trailer OEM can stand up against real hardware on day one (cargo-air temperature, door, and GPS) plus firmware scaffolding for the two paths whose engineering work is genuinely outside the scope of a reference design. The list below is the honest accounting of what's still in front of any OEM that wants to ship this as a commercial product, framed by transport and protocol choice rather than apologized for.
-
-### Outside This Reference Build's Scope
-
-The items below are the honest accounting of what's still in front of any OEM that wants to ship this as a commercial product, framed by transport and protocol choice rather than apologized for.
-
-**Reefer telemetry transport: pick one of three, then implement.** The firmware reserves `Serial1` for a reefer-telemetry source and ships with a simplified POC decode in `drainReeferUart()`. The reference assumption is J2497 PLC, but in practice three transports are realistic, and the choice changes the engineering effort substantially. The firmware's `note.add` pipeline is identical for all three; only the decode function in `drainReeferUart()` and the upstream physical interface differ.
-
- | Transport | Physical interface | Pros | Cons | Engineering required |
- |---|---|---|---|---|
- | **SAE J2497 / PLC4TRUCKS** *(reference assumption)* | Power-line carrier on J560 pin 6, via Yitran IT700 modem and Bourns coupling transformer | No additional connector to the reefer unit — rides the existing trailer power line | Custom coupling board; licensed J2497 application stack; reefer-OEM proprietary message mapping; uncommon for reefer in production (ABS is the dominant deployed J2497 use) | Build IT700 coupling board (PT60234PEL transformer + bus protection); license/integrate Microchip/Yitran J2497 stack; obtain reefer-OEM message map under integration agreement; field-validate per reefer model |
- | **Vendor-proprietary serial** (Carrier Transicold DataLink, Thermo King DSR / DSR2) | RS-232 or RS-485 diagnostic port on the reefer unit (typically a 9-pin or RJ-style connector) | Mechanically simple; documented (under NDA) at the protocol level by the OEM; most production reefer telematics use this path today | Requires a connector pass-through to the reefer chassis; protocol details are typically available only under an OEM integration agreement; per-OEM, not unified | Add an RS-232 or RS-485 transceiver between `Serial1` and the diagnostic port; obtain the protocol spec from Carrier or Thermo King; replace `drainReeferUart()` decode |
- | **J1939 over CAN** | CAN bus on the reefer unit (where exposed) | Standardized PGN-based message structure; richer telemetry (fault codes, run hours, fuel) than serial diagnostics; same bus standard widely used elsewhere on the vehicle | Requires a host with an accessible CAN controller (the STM32L433 in this build does not break out CAN on the Notecarrier CX headers. See the dedicated J1939/CAN limitation below); automotive-qualified CAN transceiver; J1939 PGN decode | Confirm host CAN pin availability or change host; add AEC-Q100 transceiver (e.g., TJA1051T/3/1J); implement J1939 PGN decode for the reefer's message set |
-
- For a first prototype against an actual reefer unit, the **vendor-proprietary serial** path is the lowest-risk first milestone: it lands on the same `Serial1` UART the reference build already exercises, and most modern reefers (Thermo King SLXe, Carrier X4 7700) expose it. The J2497 path is a credible long-term option if power-line carrier is a strategic constraint (no chassis-side connector, multi-OEM unification on a single physical interface), but expect to fund a multi-quarter effort across coupling-board hardware, stack licensing, and OEM message mapping before producing any real data.
-
-**TPMS protocol is vendor-specific.** The firmware models four tire positions with a simplified generic packet format. Commercial trailer TPMS systems (PressurePro, Doran 360, Continental VDO, TST 507) each use proprietary 315/433 MHz message structures with different tire ID encoding, pressure scaling, and checksum algorithms, so production firmware needs to implement the chosen vendor's message decode library.
-
-**Four tire positions are tracked.** A 53-foot refrigerated trailer typically runs 10 or more tires on dual rear axles, but the POC supports only four positions. Expanding to the full tire set requires a larger TPMS position array and a wider Note template.
-
-**GPS-position-delta state detection is approximate.** Comparing GPS positions at 5-minute intervals can be ambiguous in poor-sky conditions or during slow dock maneuvering. A production implementation should combine GPS position delta with the Notecard's built-in accelerometer (`card.motion.mode`) for reliable motion detection that doesn't depend on GNSS fix quality.
-
-**No J1939/CAN integration.** Modern reefer units expose richer telemetry — fault codes, fuel consumption, run hours, setpoint history — over J1939 CAN, not just temperature. Adding CAN to this platform requires more than a transceiver: (a) confirm that the STM32L433's CAN peripheral pins (CAN_TX/CAN_RX) are accessible on the Notecarrier CX/Cygnet headers (if not, a board redesign or an alternate host with CAN pins broken out is required); (b) once a host with accessible CAN pins is established, add an automotive-qualified CAN transceiver (e.g., TJA1051T/3/1J, AEC-Q100-qualified) between the host CAN controller and the J1939 bus; and (c) implement J1939 PGN (**Parameter Group Number**, the J1939 message identifier) decode in firmware (e.g., using an mcp_can-style library or a raw CAN driver targeting the host's bxCAN peripheral). **A transceiver alone is not sufficient without a host that exposes a compatible CAN controller.**
-
-**Skylo NTN satellite latency.** Satellite session establishment can take several minutes on initial power-up, and the Skylo NTN network imposes message-size and throughput constraints smaller than typical cellular sessions, so **alert Notes sent via satellite will arrive in Notehub with higher latency than cellular.** Plan monitoring workflows accordingly; Notecard for Skylo prefers cellular and falls back to satellite automatically, so this latency applies only in genuine coverage gaps.
-
-**No host over-the-air firmware update.** [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) requires SWD/BOOT pin access that is not exposed through the Notecarrier CX headers in this standard configuration. Production boards should route BOOT and NRST signals through a DFU header to enable fleet-wide host firmware updates without a service visit.
-
-**Mojo is bench tooling.** The firmware does not read the Mojo's LTC2959 coulomb counter over Qwiic. Adding a cumulative-mAh field to the summary Note is a straightforward extension that enables fleet-level power telemetry for DC dwell energy auditing.
-
-### Production Next Steps
-
-Once the three day-one sensor paths are running against real hardware, the following extensions carry the platform toward a shippable commercial product.
-
-**Full J2497 integration** comprises (1) coupling board hardware (IT700, Bourns bus-coupling transformer, bus protection), (2) a J2497 application-layer protocol stack from Microchip/Yitran or equivalent, (3) reefer-OEM message mapping per target reefer model under an integration agreement, and (4) field validation on target reefer-unit hardware to confirm J560 bus data availability.
-
-**Vendor-specific TPMS decode libraries** cover all tire positions on the OEM's chosen sensor brand.
-
-**A J1939 CAN interface** requires verifying CAN_TX/CAN_RX pin accessibility on the Notecarrier CX/Cygnet (or redesigning to a host with CAN pins exposed), adding an AEC-Q100 automotive CAN transceiver (e.g., TJA1051T/3/1J), and implementing J1939 PGN decode for comprehensive reefer-unit telemetry (fault codes, fuel, hours).
-
-**Accelerometer-augmented motion detection** (`card.motion.mode`) provides reliable parked/transit classification.
-
-**Full 10-tire TPMS coverage** supports the 53-foot dual-rear-axle configuration.
-
-**USDA and FDA FSMA-compliant temperature logging** adds cryptographically signed records with a chain-of-custody audit trail.
-
-**Host ODFU wiring** enables fleet-wide firmware updates from Notehub without a truck roll.
-
-**Humidity and CO₂ sensors** serve high-value perishable loads (fresh produce, pharmaceutical).
-
-## 11. Summary
-
-The trailer manufacturer who used to watch reefer OEMs and aftermarket platforms quietly claim the connectivity layer on every trailer they shipped now has a credible platform installed from day one: one hardware SKU across every regional market and every carrier, with the satellite gaps covered automatically and 10 years of included service that won't force a mid-program re-sourcing conversation. The Notecarrier CX with a Notecard for Skylo delivers three fully-implemented sensor paths today (cargo-air temperature, door, GPS) and the firmware scaffolding for the two vendor-gated paths (J2497 reefer telemetry and TPMS), so the platform is ready the moment that engineering lands. Between cycles the host draws zero current via `card.attn`, leaving only the Notecard's ~8–18 µA idle floor on the trailer's reefer battery; in transit it shortens its sync cadence automatically; in a coverage hole it falls back to Skylo NTN without the host ever noticing. Notehub turns the resulting stream into a single API endpoint the OEM's cloud routes wherever it needs to go (cold-chain compliance archive, TPMS safety alerts, GPS dispatch feed) over a 10–15 year trailer lifecycle, without touching firmware. That's the connectivity footprint a trailer OEM needs to own the data layer rather than concede it.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/trailer-manufacturer-connected-trailer-platform/)**
diff --git a/70-trailer-manufacturer-connected-trailer-platform/banner.png b/70-trailer-manufacturer-connected-trailer-platform/banner.png
deleted file mode 100644
index 5ed2c125..00000000
Binary files a/70-trailer-manufacturer-connected-trailer-platform/banner.png and /dev/null differ
diff --git a/70-trailer-manufacturer-connected-trailer-platform/diagrams/01-system-architecture.svg b/70-trailer-manufacturer-connected-trailer-platform/diagrams/01-system-architecture.svg
deleted file mode 100644
index b78a0792..00000000
--- a/70-trailer-manufacturer-connected-trailer-platform/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-
diff --git a/70-trailer-manufacturer-connected-trailer-platform/diagrams/02-wiring-assembly.svg b/70-trailer-manufacturer-connected-trailer-platform/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index fd406338..00000000
--- a/70-trailer-manufacturer-connected-trailer-platform/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,101 +0,0 @@
-
diff --git a/70-trailer-manufacturer-connected-trailer-platform/diagrams/03-data-flow.svg b/70-trailer-manufacturer-connected-trailer-platform/diagrams/03-data-flow.svg
deleted file mode 100644
index 344438d1..00000000
--- a/70-trailer-manufacturer-connected-trailer-platform/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/71-legacy-diesel-generator-fleet-performance-uplift/README.md b/71-legacy-diesel-generator-fleet-performance-uplift/README.md
index 682ef87f..cd88faa7 100644
--- a/71-legacy-diesel-generator-fleet-performance-uplift/README.md
+++ b/71-legacy-diesel-generator-fleet-performance-uplift/README.md
@@ -1,535 +1,5 @@
# Legacy Diesel Generator Fleet Performance Uplift
-
+A cellular Modbus retrofit for standby diesel generators that reports run hours, fault codes, fuel level, and load—staying alive on the panel's DC bus when mains and facility WiFi go dark.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Legacy%20Diesel%20Generator%20Fleet%20Performance%20Uplift).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/diesel_gen_monitor/](firmware/diesel_gen_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is an [asset performance optimization](https://blues.com/solutions-asset-performance-optimization/) retrofit for operators of standby diesel generators at hospitals, data centers, and industrial sites. The device sits on the DIN rail inside the generator panel, reads the controller's existing Modbus port, and reports run hours, fault codes, fuel level, coolant temperature, oil pressure, and load to the cloud over cellular — with immediate alerts on faults, coolant overtemp, low oil pressure, low fuel, and lost communication. Crucially, it draws from the panel's battery-backed DC control bus and uses an external antenna routed outside the metal enclosure, so the monitoring path stays alive when mains and facility WiFi go dark, which is exactly when a standby generator either starts successfully or doesn't. The hardware is an Arduino OPTA RS485 with a Blues Wireless for OPTA cellular expansion (see §3 for the BOM); the wiring is strictly read-only, with no writes to start/stop or safety circuits.
-
-## 1. Project Overview
-
-**The problem.** Standby diesel generators at hospitals, data centers, and industrial sites are among the most important assets on the premises, and among the least observed. The controllers that run them (**DeepSea** (DSE), Woodward, Kohler, Caterpillar, Cummins, and a dozen others) have spoken **Modbus RTU** (Remote Terminal Unit, the most widely deployed industrial serial protocol) for decades. That interface is sitting there, fully populated with fuel level, load percentage, coolant temperature, oil pressure, run hours, and the active alarm register that reflects the controller's current fault state — including, critically, a failure-to-start event when it is asserted. Almost none of it gets read by anyone. The controller reports faults to an annunciator panel in the same room, which is exactly where nobody is when the building is dark and the emergency has already started.
-
-The cost of this information gap is concrete. A standby generator that fails to start during a utility outage is worse than no generator at all: the false sense of protection it provides can delay emergency response. A generator that does start but is running low on fuel, or trending toward coolant overtemp, will fail mid-outage without warning. Run-hour data that never leaves the controller can't be used to schedule preventive maintenance before a critical service interval is missed. These are not exotic failure modes; they happen regularly at facilities that have generators but not generator *monitoring*.
-
-This project closes that gap for active alarm state, a firmware-observed alarm chronology, and operating data. A [Blues Wireless for OPTA](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) snapped onto an Arduino OPTA RS485 industrial **PLC** (programmable logic controller) sits on the DIN rail inside the generator panel, polls seven holding registers once per minute over the controller's Modbus port (including the active alarm bitmask), and routes alert events, an hourly alarm-history log, and hourly summaries to the [Blues Notehub](https://blues.com/notehub/) cloud service via cellular. Installation requires new RS-485 wiring to the controller's communications terminals and matching the controller's Modbus serial settings (baud rate, parity, slave address), but involves no writes to start/stop or safety circuits; the firmware is strictly read-only over Modbus. No dependency on the facility network.
-
-**Why Notecard.** The timing of this deployment is the whole point. The standby generator exists to provide power when mains power fails, which is exactly when the facility's WiFi router also goes dark. A monitoring system that relies on the building's LAN cannot report a failure-to-start at the moment that matters most. Worse, a purely LAN-dependent monitor might be relied on as evidence that everything is fine, right up until the UPS batteries run dry. Cellular with an antenna routed outside the metal generator enclosure provides a communication path that is completely independent of the facility's power and network infrastructure. The cellular path is alive precisely because it draws from the generator panel's own battery-backed DC control bus, the same battery that starts the engine.
-
-
-
-This is the key architectural insight: a well-designed generator panel already has a battery-backed 12–24 VDC control bus whose entire job is to remain live when mains fails. The OPTA and Wireless for OPTA draw from that bus. When the transfer switch opens, when the utility fails, when every office light goes out, the monitor is already running, already on cellular, and already polling the controller at one-minute intervals for exactly the data the facility manager needs.
-
-**Deployment scenario.** A single OPTA RS485 + Wireless for OPTA mounted on the DIN rail inside the generator's main control panel, powered from the panel's existing battery-backed DC control bus, RS-485 wired to the controller's communications port, cellular antenna routed out through a cable gland. RS-485 wiring and Modbus communication-setting matching are required; beyond that, no writes to safety circuits, no OEM cooperation, and no IT involvement.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The OPTA's Cortex-M7 host is the Modbus RTU **client** (master) in this conversation; the generator controller plays **server** (slave). Once a minute, the host walks the same seven holding registers over the onboard RS-485 transceiver (engine RPM, fuel %, load %, oil pressure, coolant temperature, cumulative run hours, and the active alarm bitmask) and folds the result into rolling hourly statistics held in RAM. Every poll feeds the three threshold-based alert rules and the alarm-word transition detector, so the host decides locally whether the cycle warrants an event or just contributes to the next summary. Queued [Notes](https://dev.blues.io/api-reference/glossary/#note) travel from the host to the Notecard over I²C through the Wireless for OPTA's AUX connector: no modem AT commands, no raw socket management.
-
-**Notecard responsibilities.** Each Note the host hands off lands in the Notecard's on-device queue. From there the Notecard manages everything radio-side: it brings up cellular on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence and flushes anything marked `sync:true` immediately. WiFi is a hardware capability on the NOTE-WBNAW, but production deployments leave it unconfigured on purpose: the facility WiFi access point is offline during exactly the utility failures this monitor exists to observe. The same channel runs the other direction for [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/): the facility manager retunes thresholds and per-register addresses from Notehub without anyone ever touching firmware, at least for controller families whose required points are exposed as individually readable 16-bit registers with compatible units. Vendor-specific scaling, signedness, and 32-bit fields require the production extensions described in [Limitations](#11-limitations-and-next-steps).
-
-**Notehub responsibilities.** [Notehub](https://notehub.io) is where the data lands. Events arrive over the Internet via the Notecard's embedded global SIM, every event is stored, and project-level [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) fan them out wherever the operator's downstream system needs them. Fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) let operators retune Modbus register addresses and alert thresholds without reflashing firmware. The shipped firmware performs individually-addressed 16-bit Modbus reads with no register-value scaling; vendor-specific scaling, signed-field handling beyond coolant temperature (which the demo already reads as `int16_t`), and 32-bit register handling are the production extensions described in [Limitations](#11-limitations-and-next-steps). [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) are the natural way to organize an installed base by controller family or site.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-**You will have:** A commissioning Note in Notehub confirming Modbus connectivity, hourly summaries showing generator status, and immediate alerts on controller faults.
-
-**Minimum prerequisites:** Arduino IDE or `arduino-cli` v1.3+, a generator controller with accessible Modbus port (DeepSea, Woodward, Kohler, Caterpillar, Cummins, or equivalent), baud rate and slave address from the controller's commissioning menu, and a Notehub project.
-
-**Time required:** 45 minutes (hardware assembly + wiring), 15 minutes (firmware + Notehub setup), 5 minutes (first validation). First event arrives in ~3 hours (one Modbus poll + one report boundary).
-
-**Steps:**
-
-1. **Assemble hardware:** OPTA RS485 + Wireless for OPTA on DIN rail, antenna through cable gland, powered from generator panel's battery-backed 12–24 VDC bus (fused, 3 A, per [Wiring and Assembly](#5-wiring-and-assembly) Step 2).
-2. **Wire Modbus:** OPTA `A/B/COM` to controller `A/B/COM` with 120 Ω terminators at both ends (OPTA and controller). Use twisted pair, shielded, RS-485 rated.
-3. **Flash firmware:** Copy `PRODUCT_UID` from Notehub project settings, paste into `diesel_gen_monitor.ino` line 39. Compile and upload via Arduino IDE, or:
- ```bash
- arduino-cli core install "Arduino Mbed OS Opta Boards"
- arduino-cli lib install "Blues Wireless Notecard" "ArduinoModbus" "ArduinoRS485"
- sed -i '' 's|com.your-company.your-name:diesel_gen_monitor|YOUR_PRODUCT_UID|g' firmware/diesel_gen_monitor/diesel_gen_monitor.ino
- arduino-cli compile --fqbn arduino:mbed_opta:opta_wifi firmware/diesel_gen_monitor/ --upload
- ```
-4. **Set Modbus register addresses:** In Notehub, navigate Fleet → Environment. Add variables `reg_engine_rpm`, `reg_fuel_pct`, `reg_load_pct`, `reg_oil_kpa`, `reg_coolant_c`, `reg_run_hours`, `reg_alarm_word` with the correct addresses from your controller's Modbus map. Set `modbus_baud`, `modbus_slave_id`, `modbus_parity`, `modbus_stop_bits` to match the controller's configuration. The device fetches these on the next inbound sync (120 minutes by default; to test immediately, set `inbound` to 1 minutes in `hub.set` on the device).
-5. **Validate:** Open Notehub and wait for the device to appear in the project. After the first Modbus poll (1 minutes) and report boundary (60 minutes), you'll see a `gen_summary.qo` Note with `data_ok = 1`, fuel level, alarm word, and run status. If `data_ok = 0` on all Notes, check wiring, register addresses, baud rate, and slave address against the controller's commissioning menu (see [Validation and Testing](#9-validation-and-testing) / "Modbus first-light").
-
-**Sample Note (gen_summary.qo):** Here is a sample Note this device emits:
-
-```json
-{
- "file": "gen_summary.qo",
- "body": {
- "data_ok": 1,
- "fuel_pct": 78.0,
- "load_pct": -1.0,
- "load_pct_peak": -1.0,
- "oil_kpa_mean": -1.0,
- "oil_kpa_peak": -1.0,
- "coolant_c_mean": 24,
- "coolant_c_peak": 24,
- "run_hours": 1847,
- "run_min": 0,
- "stop_min": 60,
- "engine_starts": 0,
- "alarm_word": 0,
- "alarm_word_stale": 0,
- "samples_ok": 60,
- "samples_failed": 0
- }
-}
-```
-
-**Firmware-observed alarm chronology.** The firmware polls the controller's active alarm bitmask once per `sample_minutes` (default 1 minutes) and fires a `controller_alarm` event on first observed assertion: on a zero→non-zero transition, or immediately at the first valid poll after boot if those bits are already set. Every distinct alarm-word transition (initial assertion, any mid-nonzero change where bits are added or cleared while at least one fault remains active, and the final clearance to zero) is appended to an 8-slot on-device ring buffer (`AlarmHistoryEntry`) and flushed as `gen_alarm_log.qo` at every report boundary, giving operations teams a per-window fault-set chronology. Transient faults that assert and clear entirely between two consecutive polls can still be missed. The controller's own internal timestamped alarm log (vendor-specific multi-register reads that may cover events predating the monitor's installation) is a production extension; see [Limitations](#11-limitations-and-next-steps).
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Arduino OPTA RS485](https://store.arduino.cc/products/opta-rs485) | 1 | Industrial PLC with onboard RS-485 transceiver, DIN-rail mount, 12–24 VDC supply input. Programmable with Arduino sketches. Hosts the Modbus client and all edge-logic. |
-| [Blues Wireless for OPTA (NA, SKU 992-00155-C)](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Snaps onto the OPTA's right-hand expansion port; adds a [Notecard Cell+WiFi (NOTE-WBNAW)](https://dev.blues.io/datasheets/notecard-datasheet/note-wbnaw/) over I²C. Cellular coverage is regional — pick the matching SKU for the deployment geography (EMEA variant: [SKU 992-00156-C](https://shop.blues.com/products/wireless-for-opta?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)). |
-| External cellular antenna, SMA, ~3m lead (e.g. [SparkFun CEL-16432](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html)) | 1 required, 2 recommended | Route the **primary** cellular antenna through a cable gland to the exterior of the metal generator enclosure — rubber-duck antennas inside a steel cabinet will not reliably maintain a cellular session. A **diversity** antenna on the second port improves LTE Cat-1 performance at marginal-signal sites. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Bench-only coulomb counter. Spliced inline between the power supply and the Wireless for OPTA input during commissioning to validate Notecard energy per cellular session. Not deployed to the field. |
-| 24 VDC DIN-rail supply, ≥10W (e.g. [MeanWell HDR-15-24](https://www.meanwell.com/Upload/PDF/HDR-15/HDR-15-SPEC.PDF)) | 1 | For bench testing only. In field deployments, power the OPTA and expansion from the generator panel's existing battery-backed DC control bus. The [Wireless for OPTA Quickstart](https://dev.blues.io/quickstart/wireless-for-opta-quickstart/) documents supplying 12–24 VDC to the OPTA's power terminals and jumping that same rail to the expansion's power input, confirming the full 12–24 V range is supported end-to-end with no step-up converter required. |
-
-See [§4](#4-hardware-requirements) for the BOM.
-| 120 Ω termination resistor | 1–2 | RS-485 termination at each physical end of the bus. Place one at the OPTA and one at the controller. |
-| Shielded twisted pair, 22 AWG, RS-485 rated, 1–3 m | 1 | A → A, B → B, shield → controller signal ground between the OPTA and the generator controller. Length depends on panel layout. |
-| DIN rail section, ~15 cm | 1 | Mount for the OPTA + expansion. The generator panel likely already has DIN rail. |
-| Inline fuse holder with 3 A ATO/ATC blade fuse | 1 | **Required field-installation safety item.** Wired in series with the positive supply lead from the battery-backed DC control bus before the OPTA's `+` terminal (see [Wiring and Assembly](#5-wiring-and-assembly) Step 2). Protects the control bus against a sustained hard fault in the monitoring hardware. 3 A covers the worst-case combined draw of the OPTA host (≤183 mA continuous at 12 V) plus the Notecard expansion's peak LTE burst current at the bus supply input; see [Validation and Testing](#9-validation-and-testing) for commissioning-time fuse-sizing verification. A DIN-rail fused terminal block (such as Phoenix Contact or Weidmüller series) is a cleaner alternative if the panel already accommodates DIN-rail accessories on that supply branch. |
-
-The Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-
-
-**Safety.** Generator control panels contain hazardous voltages even when the generator is stopped. Installation must be performed by qualified personnel following site lockout/tagout procedures, the generator and controller manufacturer's instructions, and applicable electrical codes. This reference design is **read-only** over Modbus; it does not command start/stop or modify any generator setpoint.
-
-
-
-1. **Mount.** Snap the OPTA RS485 onto the DIN rail. Snap the Blues Wireless for OPTA onto the OPTA's right-hand expansion port and connect the supplied solderless AUX connector between the two — this carries the I²C lines that the Notecard uses. Per the [Wireless for OPTA Quickstart](https://dev.blues.io/quickstart/wireless-for-opta-quickstart/), the expansion is not powered through USB-C; use the 12–24 VDC supply rail for any testing beyond the bench.
-
-2. **Power.** In the field: **install the 3 A inline fuse holder (see [Hardware Requirements](#4-hardware-requirements)) in series with the positive supply lead from the battery-backed DC control bus before it reaches the OPTA's `+` terminal.** This fused branch protects the control bus against a sustained hard fault in the monitoring hardware. Wire the OPTA's `+` and `−` terminals to the generator panel's battery-backed DC control bus (12 or 24 VDC as appropriate for the installation), with the fuse in the positive lead. This bus is live from the engine start battery when mains power fails — giving the monitor its independent communication path precisely when the stakes are highest. Jump the same fused supply rail to the expansion's power input terminal so the Wireless for OPTA shares the fused branch. The [Wireless for OPTA Quickstart](https://dev.blues.io/quickstart/wireless-for-opta-quickstart/) documents exactly this wiring pattern for 12–24 VDC supplies. For bench testing: wire the 24 VDC DIN-rail supply's `+V` and `GND` outputs to the OPTA's `+` and `−` terminals in the same way, then power it from mains.
-
-3. **Antennas.** Thread an SMA-female bulkhead lead through a cable gland for the primary cellular antenna and tighten it onto the first antenna port on the Wireless for OPTA. Add a second lead for the diversity antenna if the enclosure layout allows; it meaningfully improves LTE Cat-1 performance in generator rooms deep inside buildings. The bundled rubber-duck antennas are bench-only; steel enclosures kill their signal.
-
-4. **Modbus RS-485 bus.** Wire the OPTA's RS-485 terminals to the generator controller's communication port:
- - OPTA `A (+)` → Controller `A (+)` / `D+` / `TxD+` (terminology varies by vendor)
- - OPTA `B (−)` → Controller `B (−)` / `D−` / `TxD−`
- - OPTA `COM` (RS-485 GND) → Controller's documented RS-485 signal common or ground reference (consult the controller's wiring diagram); treat the cable shield/drain separately per the controller vendor's grounding guidance rather than treating it as an automatic substitute for signal common
- - Place a 120 Ω resistor across `A/B` at each physical end of the cable run: one at the OPTA, one at the controller. With one OPTA and one controller that's two terminators total. Consult the controller's Modbus commissioning guide for its specific terminal names: DeepSea 7000-series uses `+D`, `−D`; Woodward EasyGen labels them `RS485+`, `RS485−`.
-
-5. **Controller Modbus configuration.** Configure the controller as a Modbus RTU **server** (slave); the OPTA is the **client** (master). Match baud rate, parity, stop bits, and slave address in the controller's Modbus setup menu to the firmware defaults (19200 / none / 1 / slave 1), or override via `modbus_*` environment variables on Notehub.
-
-6. **Bench validation.** During first-light testing, splice the Mojo inline between the 24 VDC supply and the Wireless for OPTA power input so it measures the expansion + Notecard subsystem energy per cellular session.
-
-See [§5](#5-wiring-and-assembly) for detailed wiring instructions.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Once created, open the project dashboard and look for the **ProductUID** in the top-right corner (it looks like `com:yourcompany:projectname`). Copy it and paste it into `firmware/diesel_gen_monitor/diesel_gen_monitor.ino` line 39, replacing `com.your-company.your-name:diesel_gen_monitor`.
-
-2. **Claim the Notecard.** Power the panel; on the first cellular session the Notecard auto-provisions into your project.
-
-3. **Create a Fleet per controller family.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration and routing. Because register addresses and alarm bitmasks differ across generator controller brands, a practical structure is one fleet per controller vendor: one for DeepSea 7000-series generators, another for Woodward EasyGen, and so on. Fleet-level environment variables encode the register addresses and thresholds for that controller family; individual devices can override with their own values if a site has unusual configuration. Use [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) to dynamically route devices between fleets based on a device-level tag or environment variable.
-
-4. **Set environment variables.** All variables below are optional; firmware defaults are shown. Values set in Notehub (navigate Fleet → Environment Variables) override the compile-time defaults on the device's next inbound sync; no reflashing required.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_minutes` | `1` | Minutes between Modbus polls. |
- | `report_minutes` | `60` | Minutes between summary Notes (`gen_summary.qo`). |
- | `modbus_slave_id` | `1` | Modbus server (slave) address of the generator controller. |
- | `modbus_baud` | `19200` | Bus baud rate; must match the controller's configuration. |
- | `modbus_parity` | `none` | Parity setting: `none`, `even`, or `odd`. |
- | `modbus_stop_bits` | `1` | Stop bits: `1` or `2`. |
- | `reg_engine_rpm` | `768` | Holding-register address for engine speed (RPM). |
- | `reg_fuel_pct` | `769` | Holding-register address for fuel level (0–100%). |
- | `reg_load_pct` | `770` | Holding-register address for generator load (0–100%). |
- | `reg_oil_kpa` | `771` | Holding-register address for oil pressure (kPa). |
- | `reg_coolant_c` | `772` | Holding-register address for coolant temperature (°C, signed). |
- | `reg_run_hours` | `773` | Holding-register address for cumulative engine hours. |
- | `reg_alarm_word` | `774` | Holding-register address for the active alarm bitmask. |
- | `fuel_low_pct` | `25.0` | Fuel level (%) below which `fuel_low` fires. |
- | `coolant_alarm_c` | `95.0` | Coolant temperature (°C) above which `coolant_overtemp` fires. |
- | `oil_low_kpa` | `138.0` | Oil pressure (kPa, ≈20 psi) below which `oil_low_pressure` fires while running. |
- | `alarm_mask_fts` | `0` | Bitmask applied to `alarm_word` to detect failure-to-start events; `0` disables. Set to the decimal value of your controller's FTS bitmask (e.g. `1` for bit 0, `4` for bit 2); `0x`-prefixed hex notation is also accepted (e.g. `0x0001`). |
- | `rpm_running` | `100` | Engine RPM above which the engine is considered running for stat separation and oil-pressure evaluation. |
-
- > **Controller register-map gotchas.** The register-address defaults are illustrative for a fictional contiguous map. Real controllers differ on: 0-based vs 1-based addressing conventions; per-register scaling (oil pressure may be in 0.1 bar, tenths of kPa, or raw psi depending on the controller and configuration); signedness (coolant temperature is often signed 16-bit); 32-bit run hours that span two consecutive registers with vendor-specific word order; and active-alarm-register vs latched-alarm-history distinction. See [Limitations](#11-limitations-and-next-steps) for the production path.
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `gen_event.qo` (low-volume real-time alerts, destined for on-call paging or a **CMMS**, computerized maintenance management system) and a second for `gen_summary.qo` (long-term storage and trend analysis). Route `gen_alarm_log.qo` to the same real-time destination as `gen_event.qo` — the two share urgency (fault chronology belongs alongside alert notifications) but `gen_alarm_log.qo` is batched with the periodic outbound sync rather than triggering its own immediate cellular session. Keeping the three [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) separate at the source lets each fan out to a different destination at a different urgency without filter logic in the route.
-
-## 7. Firmware Design
-
-The firmware is a single Arduino sketch, [`firmware/diesel_gen_monitor/diesel_gen_monitor.ino`](firmware/diesel_gen_monitor/diesel_gen_monitor.ino), with the Modbus, Notecard, and statistics helpers factored into [`diesel_gen_monitor_helpers.h`](firmware/diesel_gen_monitor/diesel_gen_monitor_helpers.h) and [`diesel_gen_monitor_helpers.cpp`](firmware/diesel_gen_monitor/diesel_gen_monitor_helpers.cpp) so the orchestration in `.ino` stays readable.
-
-**Dependencies:**
-- **Arduino Mbed OS Opta Boards** core (install via the Arduino IDE Boards Manager).
-- [`Blues Wireless Notecard`](https://github.com/blues/note-arduino) (the `note-arduino` library). Install via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`. Check the [note-arduino releases page](https://github.com/blues/note-arduino/releases) for the latest version.
-- [`ArduinoModbus`](https://github.com/arduino-libraries/ArduinoModbus) and [`ArduinoRS485`](https://github.com/arduino-libraries/ArduinoRS485) (official Arduino libraries, install via Library Manager).
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard `hub.set`, template definition | `notecardConfigure`, `defineTemplates` (`.ino`) |
-| Environment-variable fetch and clamping | `fetchEnvOverrides` (`_helpers.cpp`) |
-| Modbus serial re-init on env change | `applyModbusSerialIfChanged` (`_helpers.cpp`) |
-| Hub cadence re-sync on env change | `applyHubSetIfChanged` (`_helpers.cpp`) |
-| Seven-register Modbus poll with retry | `pollGenerator`, `modbusReadOne` (`_helpers.cpp`) |
-| Rolling hourly statistics | `RollingStats` struct, `accumulate` (`_helpers.cpp`) |
-| Alarm word latch detection, history logging (`logAlarmHistory`/`flushAlarmHistory`), start-event counting | `loop()` (`.ino`) |
-| Three threshold rules + edge trigger | `evaluateRules` (`_helpers.cpp`) |
-| Immediate-sync alert emission | `sendEvent` (`.ino`) |
-| Hourly templated summary | `sendSummary` (`.ino`) |
-| Millis-based periodic scheduler (no sleep, host runs continuously on battery-backed bus) | `loop()` (`.ino`) |
-
-### Sensor reading strategy
-
-Seven holding registers are read individually using `modbusReadOne()`. Unlike pump VFD systems where registers are often contiguous and can be read in a single burst, generator controller Modbus maps vary so much between vendors that individual reads with configurable addresses are the only reliable approach. Seven reads at 19200 baud take under 500 milliseconds total, negligible for a one-minute polling cadence.
-
-All seven must succeed in a single attempt before the sample is marked valid. Partial success is silently indistinguishable from valid data with incorrect values, which is worse than no data. If any register read fails, the firmware retries up to three times before declaring the poll a failure and emitting a `modbus_unreachable` event.
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates store records as fixed-length binary rather than free-form JSON, shrinking on-wire payload 3–5×. For a fleet of 50 generators sending hourly summaries over a prepaid SIM with a finite data budget, that compression is not optional. Template binary data is decoded by Notehub and displayed as JSON in your browser or API responses.
-
-`gen_summary.qo` (periodic, default hourly). The example below shows a standby window where the engine was stopped the entire hour. This is the decoded JSON you'll see in the Notehub dashboard or when retrieving Notes via API:
-
-```json
-{
- "file": "gen_summary.qo",
- "body": {
- "data_ok": 1,
- "fuel_pct": 78.0,
- "load_pct": -1.0,
- "load_pct_peak": -1.0,
- "oil_kpa_mean": -1.0,
- "oil_kpa_peak": -1.0,
- "coolant_c_mean": 24,
- "coolant_c_peak": 24,
- "run_hours": 1847,
- "run_min": 0,
- "stop_min": 60,
- "engine_starts": 0,
- "alarm_word": 0,
- "alarm_word_stale": 0,
- "samples_ok": 60,
- "samples_failed": 0
- }
-}
-```
-
-**Schema Notes for downstream integrators:**
-
-- `data_ok`: primary validity flag. `1` means at least one successful Modbus poll was completed this window; all measurement fields are valid. `0` means a complete telemetry blackout (all `samples_failed`, no controller contact). When `data_ok = 0`, treat all computed measurement fields (`fuel_pct`, `load_pct*`, `oil_kpa*`, `coolant_c*`, `run_min`, `stop_min`) as undefined. `alarm_word` and `run_hours` are special: rather than emitting zero (which would be indistinguishable from "no alarms" or "zero hours"), the firmware carries forward the last-known values from before the blackout and sets `alarm_word_stale = 1`. See below.
-- `alarm_word_stale`: `0` under normal operation (`data_ok = 1`): `alarm_word` comes from this window's Modbus polls. `1` when `data_ok = 0`: `alarm_word` is the last-known value from before the blackout (preserved in firmware across the stats reset) and `run_hours` is the last successfully polled reading. Use `alarm_word_stale` to distinguish "no active alarms this window" from "controller was unreachable; alarm state unknown."
-- `-1.0` / `-1` sentinel: appears in running-only fields (`load_pct`, `load_pct_peak`, `oil_kpa_mean`, `oil_kpa_peak`) whenever the engine produced zero running-state samples in the window (i.e., the generator was stopped the entire hour). These fields are only accumulated when the engine is spinning, so a value of `0.0` would be indistinguishable from a true zero reading. When `data_ok = 0` the sentinel extends to all computed measurement fields including `fuel_pct` and `coolant_c_*`.
-- `samples_ok`: count of successful Modbus polls in the window (`run_samples + stop_samples`). Combined with `samples_failed`, lets downstream analytics distinguish "generator stopped, telemetry healthy" from "controller unreachable or wiring fault."
-- `samples_failed`: count of poll attempts in the window where all three Modbus retry attempts failed. Non-zero here alongside `run_min = 0` is the key signal that monitoring coverage was degraded, not that the generator simply sat idle.
-
-`gen_event.qo` (immediate, `sync:true`). Two variants below, per-poll alert and report-window alert, illustrating how `trigger_val` / `trigger_threshold` differ:
-
-**Per-poll alert** (`failure_to_start`): the freshly-polled sample already explains the trigger, so `trigger_val` and `trigger_threshold` are `-1.0`:
-
-```json
-{
- "file": "gen_event.qo",
- "body": {
- "alert": "failure_to_start",
- "engine_rpm": 0,
- "fuel_pct": 82.0,
- "load_pct": 0.0,
- "oil_kpa": 0.0,
- "coolant_c": 22,
- "alarm_word": 1,
- "run_hours": 1847,
- "trigger_val": -1.0,
- "trigger_threshold": -1.0
- }
-}
-```
-
-**Report-window alert** (`coolant_overtemp`): `trigger_val` is the window peak that crossed the threshold; `coolant_c` is the last-known sample reading (which may have subsided by report time):
-
-```json
-{
- "file": "gen_event.qo",
- "body": {
- "alert": "coolant_overtemp",
- "engine_rpm": 1500,
- "fuel_pct": 78.0,
- "load_pct": 85.0,
- "oil_kpa": 312.0,
- "coolant_c": 88,
- "alarm_word": 0,
- "run_hours": 1849,
- "trigger_val": 97.0,
- "trigger_threshold": 95.0
- }
-}
-```
-
-`gen_alarm_log.qo` (once per `report_minutes` when any alarm event occurred, batched with the periodic outbound sync, **not** template-backed):
-
-```json
-{
- "file": "gen_alarm_log.qo",
- "body": {
- "count": 2,
- "events": [
- { "alert": "controller_alarm", "alarm_word": 4, "elapsed_s": 180 },
- { "alert": "alarm_clear", "alarm_word": 0, "elapsed_s": 420 }
- ]
- }
-}
-```
-
-**Schema Notes for `gen_event.qo` downstream integrators:**
-
-- `trigger_val` / `trigger_threshold`: present in every `gen_event.qo`. For report-window rules (`fuel_low`, `coolant_overtemp`, `oil_low_pressure`), `trigger_val` is the window aggregate or peak that crossed the configured limit (`trigger_threshold`). For per-poll alerts (`controller_alarm`, `failure_to_start`, `modbus_unreachable`), both fields are `-1.0`; the existing sample fields explain the trigger directly.
-- The sample fields (`engine_rpm`, `fuel_pct`, `load_pct`, `oil_kpa`, `coolant_c`) carry the last-known polled reading at event time: closest to the trigger for per-poll alerts, machine-state context for report-window alerts. For `coolant_overtemp`, `coolant_c` may be below threshold at report time while `trigger_val` (the window peak) is above it; always use `trigger_val` to confirm the threshold crossing.
-
-**Schema Notes for `gen_alarm_log.qo` downstream integrators:**
-
-- `count`: number of entries in this flush (1–8). The ring buffer depth is 8; if more than 8 alarm transitions occur in a single report window, the oldest entries are overwritten.
-- `events[].alert`: one of `"controller_alarm"`, `"alarm_clear"`, `"failure_to_start"`, or `"fts_clear"`. Assertions (`controller_alarm`, `failure_to_start`) include the asserted `alarm_word`; clearances (`alarm_clear`, `fts_clear`) carry `alarm_word: 0`. Multiple `controller_alarm` entries with different `alarm_word` values can appear in a single flush when the active fault set changes while at least one bit remains set (e.g., a second fault asserts before the first clears, or one of several active faults clears before the others). Each entry records the exact bitmask at that poll; read them in `elapsed_s` order for the full per-window fault-set chronology.
-- `events[].elapsed_s`: seconds since device boot (`millis()/1000`) at event time. Use the Notehub-stamped Note timestamp for wall-clock time; `elapsed_s` gives relative timing between entries within one flush.
-- `gen_alarm_log.qo` is emitted only when at least one alarm event occurred in the window. Windows with no alarm transitions produce no Note. Route it to the same real-time channel as `gen_event.qo` for a complete fault chronology.
-
-### Sync and power strategy
-
-The OPTA + expansion draws continuously from the generator panel's battery-backed DC control bus — host MCU sleep is not the primary design concern here; bus efficiency and data budget are. The Notecard runs in [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `periodic` mode with `outbound` equal to `report_minutes` (default 60 minutes) and `inbound` at twice that. Summary Notes accumulate in the on-device queue and ship in a single cellular session per hour. Alert Notes set `sync:true` and ship within the session-establishment window, typically 15–60 seconds after the trigger condition is detected, regardless of where the outbound timer stands.
-
-**Battery autonomy tradeoff.** The always-on OPTA host is the dominant continuous load on a battery whose primary job is engine starting and control-circuit sustain during a mains failure. The OPTA RS485 draws 0.6–2.2 W at its supply voltage, roughly 50–183 mA at 12 V or 25–92 mA at 24 V. A 40 Ah start battery at worst-case 12 V draw (~183 mA) sustains the monitor for approximately 218 hours (~9 days) without any charge input; at best-case draw the same battery lasts roughly 800 hours. A 100 Ah battery scales proportionally. In most installations the panel's trickle charger tops up the battery continuously and any generator run event replenishes it further, so monitoring draw alone is unlikely to exhaust a healthy battery under normal site conditions. However, sites that anticipate multi-day utility outages without generator exercise or panel charging should verify battery autonomy by measuring actual host current (see [Validation](#9-validation-and-testing)) and comparing it to the site's rated battery capacity. See [Limitations](#11-limitations-and-next-steps) for production sizing guidance.
-
-### Retry and error handling
-
-- The first `hub.set` in `notecardConfigure` uses `notecard.sendRequestWithRetry()` with a 5-second window, defending against the cold-boot I²C race where the host MCU comes up before the Notecard is ready to respond.
-- Modbus reads retry up to 3× per cycle. If all three fail, the firmware skips the sample, emits a `modbus_unreachable` event Note immediately (so commissioning wiring problems surface at first light), and then rate-limits subsequent `modbus_unreachable` events to once per hour; a generator powered down for scheduled service should not flood the event log.
-- Alarm-word monitoring uses per-alert latch flags rather than raw alarm-word comparisons. A fault that stays asserted for hours fires one `controller_alarm` event; the latch blocks re-firing while the condition persists and rearms only when the word clears. Alarm fatigue is the enemy of generator monitoring.
-- `fetchEnvOverrides()` runs on every **sample interval** (default every 1 minutes) and again at every **report boundary**. This re-reads the Notecard's local environment-variable cache, but that cache is only refreshed when Notehub delivers an inbound sync. The Notecard's `inbound` cadence is configured to `report_minutes * 2` (120 minutes at the default `report_minutes = 60`), so the worst-case propagation delay from a Notehub env-var change to the device is up to one full `inbound` interval (120 minutes by default). Once an inbound sync occurs and the cache is updated, the new values take effect within one sample period. The one exception is `sample_minutes`: a new value is stored in `g_pending_sample_minutes` and promoted to the active cadence only when the current window closes with a confirmed `note.add`, ensuring `run_min` / `stop_min` always reflect one consistent cadence per window. At the report boundary, `applyHubSetIfChanged()` re-issues `hub.set` if `report_minutes` changed, keeping the Notecard's outbound cadence in sync with the local summary cadence.
-
-### Key code snippet 1: latch-based alarm detection, retry-on-send-failure, and alarm history logging
-
-The alarm register is checked on every successful poll. Detection is **latch-based**, not transition-gated: the condition is evaluated against the per-alert latch flags (`g_active_controller_alarm`, `g_active_fts`) rather than against a change in the stored alarm word. This means that if `sendEvent()` fails — transient I²C hiccup, Notecard not yet ready — the latch stays `false` and the alert is automatically retried on every subsequent sample while the fault remains asserted. Once the Note is queued successfully the latch blocks re-firing, so a sustained fault still emits only one event per asserted-alarm period. `g_current_alarm_word` is updated unconditionally on every sample, independently of the latch state, so the summary Note always carries the freshest alarm word.
-
-History logging captures every distinct `alarm_word` value, not just the initial assertion. A separate `g_alarm_logged_controller` / `g_alarm_logged_fts` flag tracks whether the first entry for a given assertion period has been written; the initial detection logs exactly once through that gate. While the alarm word remains non-zero, any subsequent poll that returns a *different* nonzero value — bits added as a second fault asserts, or one of several bits cleared while others persist — is logged immediately as an additional `controller_alarm` entry with the updated bitmask. No new `sendEvent()` is triggered for these mid-nonzero changes; one alert per assertion period avoids alarm fatigue on gradually-evolving fault sets. Decoupling history-logged state from event-queued state means that when `sendEvent()` fails and the latch stays `false`, the next retry re-calls `sendEvent()` but does **not** re-log the initial entry; without this separation, send retries would accumulate duplicate entries, corrupting the chronology and evicting later events from the 8-slot buffer. The matching clearance (`alarm_clear` / `fts_clear`) is logged once when the alarm word returns to zero, gated on the history flag rather than the event-queued latch, so a clearance entry is always paired with its assertion even when the Note never queued successfully.
-
-**Boot-seeding behavior.** On the *first* valid poll after boot or a watchdog reset, the firmware checks the alarm word immediately and emits `controller_alarm` (and `failure_to_start` if `alarm_mask_fts` is configured) if those bits are already asserted — without waiting for a zero-to-nonzero transition that may never occur on this boot session. This closes the gap where a monitor rebooted mid-outage would otherwise suppress the pre-existing fault. The boot-seeding block calls `logAlarmHistory()` and sets the corresponding `g_alarm_logged_*` flag so the main detection loop does not add a second history entry on the same assertion period if `sendEvent()` fails at boot.
-
-```cpp
-// Three-state detection: 0→nonzero (assertion), nonzero→different-nonzero
-// (fault-set change while still alarming), nonzero→0 (clearance).
-// Events are latch-based: sendEvent() fires once per assertion period; failed
-// sends retry automatically while the condition persists without duplicate entries.
-// History logs every distinct alarm_word value for a complete fault-set chronology.
-if (s.alarm_word != 0 && !g_active_controller_alarm) {
- if (!g_alarm_logged_controller) {
- logAlarmHistory("controller_alarm", s.alarm_word); // log once on first observation
- g_alarm_logged_controller = true;
- }
- if (sendEvent("controller_alarm", &s)) {
- g_active_controller_alarm = true; // latch: suppress repeats until clear
- }
-} else if (s.alarm_word != 0 && g_active_controller_alarm &&
- s.alarm_word != g_current_alarm_word) {
- // Fault bits changed while at least one remains asserted: log the updated
- // bitmask to capture the evolving fault set. No sendEvent() — one alert
- // per assertion period.
- logAlarmHistory("controller_alarm", s.alarm_word);
-} else if (s.alarm_word == 0) {
- if (g_alarm_logged_controller) {
- logAlarmHistory("alarm_clear", 0); // record clearance once
- }
- g_active_controller_alarm = false; // rearm for the next assertion
- g_alarm_logged_controller = false; // rearm history flag
-}
-
-// Failure-to-start: tracks the FTS bitmask independently, same latch semantics.
-if (g_alarm_mask_fts != 0) {
- bool fts_active = ((s.alarm_word & g_alarm_mask_fts) != 0);
- if (fts_active && !g_active_fts) {
- if (!g_alarm_logged_fts) {
- logAlarmHistory("failure_to_start", s.alarm_word); // log once on first observation
- g_alarm_logged_fts = true;
- }
- if (sendEvent("failure_to_start", &s)) {
- g_active_fts = true;
- }
- } else if (!fts_active) {
- if (g_alarm_logged_fts) { logAlarmHistory("fts_clear", 0); }
- g_active_fts = false; // rearm when FTS bits clear
- g_alarm_logged_fts = false; // rearm history flag
- }
-}
-```
-
-### Key code snippet 2: immediate-sync alert with trigger fields
-
-`sync:true` instructs the Notecard to bypass the outbound interval and open a cellular session immediately. The sample source and trigger fields differ by event type:
-
-- `controller_alarm` and `failure_to_start` pass the freshly-polled sample directly: closest available reading to the moment the alarm was detected. `trigger_val` and `trigger_threshold` default to `-1.0` (not applicable).
-- `modbus_unreachable` carries `g_last_known_sample` when a valid prior sample exists, or null/zero fields if no successful poll has been completed yet.
-- Report-boundary threshold rules (`fuel_low`, `coolant_overtemp`, `oil_low_pressure`) pass `g_last_known_sample` as machine-state context **plus** `trigger_val` (the window aggregate or peak that crossed the threshold) and `trigger_threshold` (the configured limit). This makes every report-window alert self-explaining: a `coolant_overtemp` event shows `trigger_val: 97.0` and `trigger_threshold: 95.0` even when the last-known `coolant_c` sample has already subsided below 95°C.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "gen_event.qo");
-JAddBoolToObject (req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "fuel_low");
-JAddNumberToObject(body, "fuel_pct", s->fuel_pct); // last-known sample
-JAddNumberToObject(body, "engine_rpm", s->engine_rpm);
-JAddNumberToObject(body, "alarm_word", s->alarm_word);
-JAddNumberToObject(body, "run_hours", s->run_hours);
-JAddNumberToObject(body, "trigger_val", fuel_mean); // window mean that fired the rule
-JAddNumberToObject(body, "trigger_threshold", g_fuel_low_pct); // configured threshold
-notecard.requestAndResponse(req);
-```
-
-### Key code snippet 3: oil-pressure rule (running-only, with trigger fields)
-
-Oil pressure is only meaningful when the engine is spinning. The rule is skipped entirely if the engine produced zero running-state samples in the window, preventing constant false positives on a generator sitting in standby for days at a time. `trigger_val` carries the window mean that crossed the threshold; `trigger_threshold` is the configured limit. The last-known sample fields provide current machine-state context.
-
-```cpp
-if (stats.run_samples > 0) {
- float oil_mean = stats.oil_sum / (float)stats.run_samples;
- if (oil_mean < g_oil_low_kpa) {
- if (!g_active_oil_low) {
- if (sendEvent("oil_low_pressure", &g_last_known_sample,
- oil_mean, g_oil_low_kpa)) {
- g_active_oil_low = true;
- }
- }
- } else {
- g_active_oil_low = false;
- }
-}
-```
-
-## 8. Data Flow
-
-
-
-**Collected.** Every `sample_minutes` (default 1 minutes): engine RPM, fuel level %, generator load %, oil pressure kPa, coolant temperature °C, cumulative run hours, active alarm bitmask. Each valid poll also updates the running/stopped sample counts and, on an RPM threshold crossing, increments the engine-start counter.
-
-**Summarized.** Every `report_minutes` (default 60 minutes): mean fuel level; mean and peak load and oil pressure (running-state samples only); mean and peak coolant temperature; cumulative run hours; minutes running and stopped this window; engine start count; latest alarm-word polled in the window (non-zero whenever any alarm was active at the time of the final sample in the window, a fault sustained across a report boundary will appear as non-zero in the next summary).
-
-**Transmitted.**
-- `gen_summary.qo`: once per `report_minutes`, queued and batched with the Notecard's periodic outbound sync. Template-encoded to minimize on-wire size.
-- `gen_event.qo`: emitted immediately with `sync:true` on any rule trigger. One event per condition transition, not one per sample interval.
-- `gen_alarm_log.qo`: once per `report_minutes` when any alarm-word transition (assertion or clearance) occurred in the window. Full JSON (not template-encoded; array length varies). Batched with the periodic outbound sync. Omitted for windows with no alarm activity.
-
-**Routed.** Notehub fans `gen_event.qo` and `gen_alarm_log.qo` to the real-time channel the operator uses (CMMS ticket creation, on-call paging, building management system webhook, etc.). `gen_event.qo` arrives immediately via `sync:true`; `gen_alarm_log.qo` is batched with the next periodic outbound sync and delivers the per-window alarm chronology that complements the event stream. Both should share the same downstream destination so fault chronology and event notifications land in the same place. `gen_summary.qo` routes separately to a long-term store for fleet trend analysis.
-
-**Triggers.** Six conditions generate alert events, split into two timing categories:
-
-*Per-poll alerts*, evaluated on every successful Modbus read; latency is approximately one `sample_minutes` interval (default 1 minutes):
-- `controller_alarm`: the active alarm word is first observed non-zero: on a zero→non-zero transition, or immediately at the first valid poll after boot if already asserted. One event fires per asserted-alarm period; the flag rearms when the alarm word returns to zero. Catches any fault condition the controller's alarm map defines: failure-to-start, under/over voltage, over-speed, high temperature, low coolant level, and others. Every assertion and clearance is also appended to the local alarm-history ring buffer and flushed as `gen_alarm_log.qo`.
-- `failure_to_start`: a subset of `controller_alarm` targeting the specific bit(s) the operator identifies via `alarm_mask_fts`. Disabled by default; configure per controller family.
-- `modbus_unreachable`: all three Modbus retry attempts failed. First occurrence fires immediately; subsequent occurrences are rate-limited to once per hour.
-
-*Report-window alerts*, evaluated once per `report_minutes` (default 60 minutes) against the window's rolling aggregates; default latency is up to one full report interval from the triggering condition:
-- `fuel_low`: window-mean fuel level below `fuel_low_pct`. Edge-triggered; rearms when fuel rises above threshold (e.g., after a refill).
-- `coolant_overtemp`: window-peak coolant temperature above `coolant_alarm_c`. Peak, not mean, because an overheat event can spike and resolve within a single report window.
-- `oil_low_pressure`: window-mean oil pressure below `oil_low_kpa` **while the engine was running**. Only evaluated when `run_samples > 0`.
-
-## 9. Validation and Testing
-
-**Expected steady-state behavior.** A correctly-commissioned generator in standby should produce one `gen_summary.qo` event per hour with `run_min = 0`, `engine_starts = 0`, and `alarm_word = 0`. During a weekly exercise test (typically 30 minutes), the summary windows that straddle the test will show non-zero `run_min`, `engine_starts = 1` in the window that captured the start, and non-zero `load_pct` and `oil_kpa_mean`. Verifying that pattern during the first scheduled test after installation is the primary validation step.
-
-**Modbus first-light.** Before connecting to the real controller, run the firmware against a USB-to-RS-485 adapter and a software Modbus simulator (Modbus Mechanic, ModRSsim2, or equivalent) wired to the OPTA's RS-485 terminals. Confirm the seven register reads arrive at the expected addresses. Note that the firmware's serial output shows Modbus retry failures and Notecard debug traffic only — it does not print decoded register values. To inspect register contents during bench bring-up, either temporarily add `Serial.print` statements inside `pollGenerator()` or verify the data by watching `gen_summary.qo` Notes appear in Notehub after the first report boundary.
-
-**Fault simulation.** With a real controller connected, temporarily set `fuel_low_pct` to `100` in the fleet's environment variables; on the next inbound sync the device will pull the new value, and the subsequent report boundary will trip `fuel_low` regardless of actual fuel level. Verify the event appears in Notehub within the session-establishment window. Restore the original value to rearm.
-
-**Power validation with Mojo.** Spliced inline between the 24 VDC supply and the Wireless for OPTA power input, the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) reports cumulative mAh at 1% accuracy over its Qwiic connector. This placement covers the Wireless for OPTA expansion subsystem (Notecard plus the expansion's onboard regulators and I²C glue) — it does **not** measure the OPTA host, which is powered directly from the same control bus through its own `+/−` supply terminals. The Notecard-subsystem figures below are from the [NOTE-WBNAW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-wbnaw/) and the [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/):
-
-| Phase | Notecard subsystem current (Mojo measurement) |
-|---|---|
-| Notecard idle (radio off, between syncs) | ~8–18 µA @ 5V |
-| Modem active (cellular session, LTE Cat-1) | ~250 mA average, ≤2 A peak bursts |
-| WiFi active (hardware capability; leave unconfigured in production) | ~80 mA average |
-
-The OPTA host runs continuously — the firmware's millis-based scheduler keeps the Cortex-M7 awake with no sleep state. The [OPTA RS485 datasheet](https://docs.arduino.cc/hardware/opta/) specifies 0.6–2.2 W at 24 VDC (approximately 25–92 mA at 24 V; roughly 50–183 mA at 12 V), which is the dominant steady-state load on the control bus and must be measured separately. To do so, splice an ammeter or a second Mojo between the control-bus supply and the OPTA's `+/−` terminals during bench bring-up.
-
-Confirm on the Mojo trace: idle current between syncs is in the µA range. During hourly sync sessions the modem averages ~250 mA but may spike to ≤2 A in brief RF bursts — this is normal LTE Cat-1 behavior, not a fault. Rather than watching for a hard instantaneous-current ceiling, validate **energy per session**: a healthy hourly sync should accumulate only a few mAh on the Mojo counter; the idle periods between syncs should show negligible accumulation. Alert events (`sync:true`) produce additional modem sessions between the scheduled syncs, each with the same average-then-burst current profile. If the device shows continuous elevated current with no idle periods, the Notecard's periodic mode is not configured correctly. See the [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for the conditions required to reach the lowest-power idle state.
-
-In the field, the generator's start battery sustains the control bus through a mains failure, but it is not an infinite reservoir. The dominant continuous load is the OPTA host (0.6–2.2 W per the datasheet); the Notecard expansion's 8–18 µA idle draw is negligible alongside it. Use the ammeter or second Mojo measurement described above to record actual OPTA host current during commissioning, then compare it against the site's rated battery capacity to bound worst-case autonomy, particularly relevant at 12 V sites with smaller batteries. The 3 A fuse required in [Hardware Requirements](#4-hardware-requirements) and installed per [Wiring and Assembly](#5-wiring-and-assembly) Step 2 is sized for worst-case 12 V operation: the OPTA host draws up to ≤183 mA continuous and the Notecard expansion can burst to ≤2 A at the bus input, for a combined worst-case total of ≤2.2 A. To verify sizing at your specific installation, add the measured OPTA supply current to the peak Notecard-expansion burst current (≤2 A) from the Mojo trace and confirm the combined total remains below the installed fuse rating. Sites with 24 V supplies see proportionally lower bus currents, making the 3 A rating conservative; do not omit the fuse.
-
-## 10. Troubleshooting
-
-**Device appears in Notehub but all `gen_summary.qo` Notes show `data_ok = 0`**
-- Modbus polling failed on all retries. Check wiring: are OPTA `A/B/COM` correctly wired to the controller's corresponding terminals? Use a multimeter to verify continuity.
-- Baud rate mismatch. Compare `modbus_baud`, `modbus_parity`, and `modbus_stop_bits` in Notehub (Fleet → Environment) against the controller's Modbus commissioning menu. They must match exactly.
-- Slave address mismatch. Verify `modbus_slave_id` in Notehub matches the controller's configured address (often found in Modbus or network setup menus, defaults to 1).
-- Missing or incorrect termination resistors. Confirm 120 Ω resistors are installed at both physical ends of the RS-485 cable run: at the OPTA and at the controller.
-- First-light validation: before commissioning on the real controller, test with a USB-to-RS-485 adapter and a Modbus simulator (Modbus Mechanic, ModRSsim2) to confirm the firmware's Modbus reads work at all (see [Validation and Testing](#9-validation-and-testing)).
-
-**Register addresses appear wrong or show `−1.0`**
-- Wrong register addresses. The defaults (768–774) are illustrative for a fictional map. Look up your **controller vendor and model** in its Modbus datasheet and set `reg_engine_rpm`, `reg_fuel_pct`, etc. in Notehub (Fleet → Environment) to the correct addresses for your specific controller.
-- `-1.0` sentinels mean the engine was stopped the entire report window (normal for a standby generator). They appear in running-only fields (`load_pct`, `oil_kpa_mean`). A fuel level of `-1.0` or `data_ok = 0` indicates a communication failure, not a sensor issue.
-
-**Antenna connectivity issues**
-- Rubber-duck antennas don't work inside metal generator enclosures. Route the primary cellular antenna (SMA) through a cable gland to the outside. Add a second diversity antenna if signal is marginal.
-- Antenna not seated or damaged. Verify the SMA connector is tightened fully onto the antenna port.
-
-**Alert events not appearing in Notehub**
-- Environment variables not applied. Changes set in Notehub take effect on the device's next **inbound sync**, not immediately. The default `inbound` cadence is 120 minutes; to test quickly, temporarily set it to 1 minutes via `hub.set` on the device or via a direct Notehub API call, then revert after testing.
-- Rule thresholds not realistic. If `fuel_low_pct` is set to 100, the device will trigger `fuel_low` on every summary. Verify threshold values match your site's expectations (e.g., `fuel_low_pct = 25.0` for "alert when fuel falls below 25%").
-
-## 11. Limitations and Next Steps
-
-This reference design is deliberately scoped to the path that takes a facility manager from a generator with a Modbus port to a generator with a live cellular uplink, in well under a day, without writing anything back to the controller. A handful of details were left simple so that path stays clean; each is documented below with the production extension that closes the gap.
-
-### Simplified for this reference design
-
-Each item below is a place where the reference firmware keeps things deliberately generic, with the production extension that a real fleet deployment will reach for once it is running against actual controllers.
-
-**Register addresses, scaling, and signedness** are illustrative for a fictional contiguous map. Each controller vendor publishes its own Modbus map, so commissioning a real site means looking up actual addresses, scaling factors (oil pressure may be in 0.1 bar, hundredths of kPa, or raw integer psi), signedness (coolant temperature is already read as signed `int16_t` in the demo; oil pressure and other fields on some controllers may also be signed and require case-by-case handling), word counts (run hours on DeepSea 7000-series is a 32-bit value spanning two consecutive registers), and addressing convention (0-based wire-level vs 1-based / Modicon "40001" notation). The shipped firmware reads seven individually-addressed 16-bit registers with no scaling; **production builds need vendor-specific handling.**
-
-**Firmware-observed alarm history, not controller-internal log.** The firmware tracks its own alarm-history log: every alarm assertion and clearance it detects is appended to an 8-slot ring buffer and flushed as `gen_alarm_log.qo` at each report boundary (see §6 and §7). What remains a future enhancement is reading the controller's own internal timestamped fault log; most production controllers maintain a multi-register circular buffer of events that can include faults that pre-date the monitor's installation. Extracting that log requires a vendor-specific read sequence; see Production Next Steps below.
-
-**Sampled alarm detection, not latched.** The alarm word is polled once per `sample_minutes` (default 1 minutes), so **a transient fault that asserts and clears entirely between two consecutive polls will be invisible to the firmware.** Controllers that do not latch faults internally require a short enough polling cadence to catch the briefest expected fault pulse; for controllers that do latch faults, reading the alarm history log (above) is the robust alternative.
-
-**Failure-to-start detection requires operator configuration.** The `alarm_mask_fts` variable is `0` (disabled) by default because bit positions differ across controller families: a DeepSea 7000-series uses different alarm bits than a Woodward EasyGen. The operator must look up their controller's alarm register map and set the appropriate mask. Until configured, failure-to-start events are caught by the generic `controller_alarm` alert.
-
-**Single controller per OPTA.** The firmware reads one Modbus slave ID, so a facility with multiple generators requires one OPTA + Wireless for OPTA per generator, or a firmware extension to round-robin across slave IDs on the same bus (with per-slave stat tracking).
-
-**Weekly test coverage** can be straddled by an hourly summary. A 30-minute test that starts and finishes within a single 60-minute summary window will appear in that window's data, but the start event and any transient fault that clears during the test will be visible only via the event Notefile. Production deployments could add logic to emit a summary at engine stop (end of each run event) for complete run-level granularity.
-
-**No Modbus writes.** The firmware is read-only. Sending remote start commands to the generator is **out of scope**; that requires safety analysis, E-stop wiring, and potentially functional-safety certification.
-
-**Continuous OPTA host draw on the start battery** is a consequence of running no sleep state: the OPTA's Cortex-M7 stays continuously awake, drawing 0.6–2.2 W on the battery-backed control bus. Panel trickle chargers handle this load under normal operation, but a site that experiences extended utility outages without generator exercise or charger input should measure the actual OPTA supply current during commissioning and verify that the site's battery capacity provides acceptable autonomy. At 12 V and worst-case 2.2 W draw (~183 mA), a 40 Ah battery sustains the monitor alone for roughly 9 days; at best-case draw the same battery extends to ~33 days. Larger batteries and 24 V systems extend autonomy proportionally.
-
-### Production Next Steps
-
-Once a real fleet is reporting in, the following extensions are the natural progression toward a commercial-grade monitoring product.
-
-**Vendor-specific register-map builds** cover DeepSea 7000/8000, Woodward EasyGen 3000, Kohler RDC, Caterpillar EMCP 4, and Cummins PowerCommand, each with the correct addresses, scaling, signedness, and 32-bit run-hour handling for that family.
-
-**Controller-internal alarm log readout** per vendor spec (DeepSea Event Log registers, Woodward EasyGen fault record, Kohler RDC history block, etc.) supplements the firmware-observed history with timestamped events that pre-date the monitor's installation or that occurred while the firmware was offline.
-
-**Per-controller baseline learning** tracks run hours and load history per device to detect drift toward scheduled service intervals rather than waiting for a threshold breach.
-
-**Over-the-air firmware updates** via [Notecard Outboard Firmware Update](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) let a service company push a new register-map build to an entire fleet without a truck roll. (Note: ODFU on the OPTA requires AUX wiring that Wireless for OPTA does not currently break out; local USB-C update is the current path for host firmware changes.)
-
-**Fuel consumption rate** can be derived from the rate of change in fuel level across run events. Combined with load data, this gives a fuel-per-kWh efficiency figure that accumulates across the maintenance history.
-
-**Automatic weekly test verification** flags a `test_not_detected` event if no engine start is observed in a configurable rolling window, catching test skips before an audit or inspection.
-
-## 12. Summary
-
-For the facility manager who used to find out about a failed-to-start generator from a phone call after the UPS batteries gave up, the picture is now reversed: fault transitions and threshold trips land in real time, weekly exercise tests leave a clear signature in the hourly summaries (`engine_starts`, `run_min`, `load_pct`, and `oil_kpa_mean` all populate the window that captured the test), and the run-hour and load history needed to get ahead of maintenance intervals streams in continuously. The controller inside that enclosure has been measuring fuel level, coolant temperature, oil pressure, load, run hours, and active alarm state for years; what was missing was a communication path that works when mains is dark, the WiFi router is dead, and the generator either starts or doesn't. An OPTA RS485 with Blues Wireless for OPTA, powered from the generator's own battery-backed control bus, provides exactly that path: a cellular uplink structurally independent of facility power and completely indifferent to whether the WiFi router is working. Automatic detection of a missed test (flagging `test_not_detected` when no start is observed in a configurable rolling window) is a planned next step; see [Limitations](#11-limitations-and-next-steps). The generator doesn't change. Only the information does.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/legacy-diesel-generator-fleet-performance-uplift/)**
diff --git a/71-legacy-diesel-generator-fleet-performance-uplift/banner.png b/71-legacy-diesel-generator-fleet-performance-uplift/banner.png
deleted file mode 100644
index 3badcd12..00000000
Binary files a/71-legacy-diesel-generator-fleet-performance-uplift/banner.png and /dev/null differ
diff --git a/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/01-system-architecture.svg b/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/01-system-architecture.svg
deleted file mode 100644
index 50f66f19..00000000
--- a/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,67 +0,0 @@
-
diff --git a/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/02-wiring-assembly.svg b/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index ba037f7a..00000000
--- a/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,86 +0,0 @@
-
diff --git a/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/03-data-flow.svg b/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/03-data-flow.svg
deleted file mode 100644
index 21c9c221..00000000
--- a/71-legacy-diesel-generator-fleet-performance-uplift/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,123 +0,0 @@
-
diff --git a/72-solar-array-string-level-performance-dashboard/README.md b/72-solar-array-string-level-performance-dashboard/README.md
index 470e45c2..fa0c6fc5 100644
--- a/72-solar-array-string-level-performance-dashboard/README.md
+++ b/72-solar-array-string-level-performance-dashboard/README.md
@@ -1,502 +1,5 @@
# Solar Array String-Level Performance Dashboard
-
+A cellular reference design that turns a solar array into a per-string, continuously-monitored asset for detecting soiling, shading, and bad modules without WiFi or site IT involvement.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Solar%20Array%20String-Level%20Performance%20Dashboard).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/solar_string_monitor/](firmware/solar_string_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is an [asset performance optimization](https://blues.com/solutions-asset-performance-optimization/) reference design that turns a solar array into a per-string, continuously-monitored asset. It catches soiling, shading, and bad modules from wherever the array happens to be, without WiFi, without site IT involvement, and without touching the existing inverter.
-
-## 1. Project Overview
-
-**The problem.** Utility-scale and large commercial-and-industrial (C&I) solar installations routinely bleed 3–8% of annual production to three mundane, entirely preventable causes: soiling (dust, bird droppings, pollen), partial shading (a branch that grew six inches over winter, a newly-installed HVAC unit on a flat roof), and a single degraded or failed module in a series string. None of these losses are invisible; they all have distinctive electrical signatures in the per-string DC current and voltage data. The problem is that almost nobody reads that data in real time.
-
-A modern string inverter or combiner box already knows, at the register level, exactly how much DC current and voltage each string is producing. What it doesn't have is a network path off the array and into the asset manager's dashboard. The maintenance tech has to show up on-site with a laptop and a Modbus cable to pull that data, which is exactly what doesn't happen until something breaks badly enough to trigger a service ticket. The result is months of invisible partial production loss that neither the owner nor the O&M (operations and maintenance) contractor can see until the quarterly energy report shows a yield gap.
-
-This project adds that missing network path. It reads per-string DC voltage and current from a Modbus RTU source, typically a multi-MPPT string inverter where each MPPT input tracks one string — every five minutes, reads the irradiance from a pyranometer (a calibrated instrument that measures incident solar radiation in watts per square meter, W/m²) and the panel temperature from a backsheet probe, and computes a **Performance Ratio** (**PR**) — the ratio of actual string power to the expected power given current irradiance and temperature — for each string independently. The root-cause hypothesis (shading, soiling, string fault) depends on having an independent operating voltage reading per string; multi-MPPT inverters implementing the [SunSpec Model 160 Multiple MPPT](https://sunspec.org/sunspec-modbus-specifications/) register model provide this signal set. See [§11](#11-limitations-and-next-steps) for hardware compatibility and the implications for deployments where only per-string current is available from the monitored device. Strings that fall below a configurable PR threshold get an immediate alert with a root-cause hypothesis. When a string's PR recovers above the threshold, the device locally rearms the alert — the internal underperformance flag is cleared so a subsequent degradation fires a fresh event, but no recovery Note is sent to the [Blues Notehub](https://blues.com/notehub/) cloud service. Every hour a summary Note records the per-string window means and the array-level shared-reference irradiance and module temperature for trend analysis.
-
-**Why Notecard.** Solar arrays sit on rooftops, in open fields, and under parking canopies: three of the places where WiFi ranges the least and site IT involvement is the highest hurdle. A rooftop system at a strip mall isn't going to get a permanent AP installed on the ballast tray. A ground mount in a rural field has no building nearby. A carport canopy shared by a retail parking lot has a network owned by the tenant's coffee franchise, not the solar O&M contractor. Cellular removes every one of those constraints. The Notecard Cell+WiFi variant ships with a prepaid global SIM and registers on the cellular network automatically: no SIM activation form, no per-site IT discussion, and no router credentials to manage. The WiFi radio is present in the hardware but is not used in this build: a metal NEMA 4X enclosure substantially attenuates WiFi signals, and the firmware explicitly clears any previously stored WiFi credentials at first boot via `card.wifi`. This ensures cellular-only operation even on a reused or previously provisioned Notecard rather than relying on an assumption about the device's prior state. For a portfolio O&M operator managing dozens of customer sites, the single most valuable thing about cellular-first IoT is that the same firmware and SKU deploys identically on site one and site forty-three. There is no site-specific network configuration at all.
-
-
-
-**Deployment scenario.** A metal NEMA 4X enclosure mounted on or near the combiner box or string inverter, powered from the array's AC output via a compact AC/DC converter. Three cables exit the enclosure: one RS-485 shielded pair to the combiner's Modbus port, one pair for the pyranometer, and the DS18B20 backsheet temperature probe cable routed to a representative panel in the array. No inverter modification and no plant-network involvement required.
-
-## Before You Start — Critical Constraints
-
-**Per-string independent voltage required.** This firmware reads a [V, I] register pair per string and classifies root causes by comparing each string's operating voltage and current to the fleet mean. Multi-MPPT inverters (implementing [SunSpec Model 160 Multiple MPPT](https://sunspec.org/sunspec-modbus-specifications/)) expose per-MPPT voltage and current; these work perfectly. Traditional string combiners aggregate multiple strings onto a shared DC bus and expose only per-string current, not per-string voltage. Against a combiner-only source, shading root-cause classification will never fire. See [§11 Limitations](#11-limitations-and-next-steps) for details.
-
-**Maximum 4 strings supported.** The compile-time constant `MAX_STRINGS = 4` fits the STM32L433's 64 KB SRAM budget. Scaling beyond 4 requires revalidating static memory footprint and is untested.
-
-**Modbus register map is a demo.** The firmware reads contiguous holding-register pairs `[V, I, V, I, …]` with fixed scaling. Real inverters (SMA SunnyBoy, Fronius Symo, Huawei SUN2000, etc.) have vendor-specific register maps, addressing conventions, and scaling. Production deployments require a vendor-specific firmware build or careful manual register validation (see Modbus first-light in §9).
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Every five minutes the Cygnet STM32L433 host on the Notecarrier CX walks all three inputs in turn: per-string DC voltage and current over Modbus RTU, irradiance from the pyranometer's analog output, and module temperature from the 1-Wire probe. With those three readings in hand it computes a temperature-derated expected power for each string and turns the result into a Performance Ratio the host can compare against the alert threshold locally. From there the cycle has one of two endings: either a fresh summary Note is added to the queue, or, if a string has fallen below threshold, an immediate alert fires. Everything moves over I²C to the Notecard sitting in the carrier's M.2 slot. No modem AT commands, no session management, no raw socket code.
-
-**Notecard responsibilities.** The Notecard takes everything from there. It queues [Notes](https://dev.blues.io/api-reference/glossary/#note) in on-device flash, brings up cellular sessions on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes), and wakes the radio immediately for anything marked `sync:true`. The same channel runs the other direction for [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/): an O&M operator can retune thresholds, register addresses, and scaling factors across an entire portfolio's worth of devices from Notehub without anyone touching firmware.
-
-**Notehub responsibilities.** [Notehub](https://dev.blues.io/notehub/notehub-walkthrough/) is where the data lands. Events arrive over the Internet, every event is stored, and project-level [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) fan them out to wherever the operator's downstream system needs them. Summaries and alerts land in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) so each stream can go to a different destination at a different urgency, no filter logic required. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) are how a multi-site O&M operator slices the portfolio (one fleet per customer, or one fleet per panel technology such as monocrystalline vs bifacial), each carrying its own environment-variable overrides without ever forking the firmware.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-**What you'll have when you're done:** A cellular-connected device that reads per-string DC voltage and current from your Modbus inverter or combiner every 5 minutes, computes each string's performance ratio (actual power / temperature-derated expected power), fires an immediate alert to [Notehub](https://notehub.io) when any string drops below 80% of expected power (with a root-cause hypothesis: shading, soiling, or string fault), and sends an hourly summary Note for trend analysis.
-
-**Fastest path to first event (30 minutes):**
-1. Assemble hardware: Notecarrier CX + Notecard Cell+WiFi + RS-485 transceiver + sensors (see §5 Wiring and Assembly).
-2. Claim project in Notehub (§6 step 1–2).
-3. Copy the firmware directory and edit `solar_string_monitor.ino` to replace `PRODUCT_UID` with your Notehub project UID.
-4. Flash via Arduino IDE or CLI (see Build Instructions below).
-5. Power the assembly; on first boot the Notecard registers with your project automatically.
-6. Check Notehub. You should see `solar_summary.qo` and/or `solar_alert.qo` events appear within the next sample interval.
-
-**Build Instructions:**
-
-Using the Arduino IDE:
-- Install the [Arduino core for STM32](https://github.com/stm32duino/Arduino_Core_STM32) via Board Manager.
-- Sketch → Include Library → Manage Libraries, then install: **Notecard**, **ModbusMaster**, **OneWire**, **DallasTemperature**.
-- Select Board: **Blues Cygnet** (the Notecarrier CX's embedded host is the Blues Cygnet — STM32L433-based), and Port (your debugger/programmer).
-- Paste your ProductUID into the `PRODUCT_UID` define.
-- Upload.
-
-Using Arduino CLI:
-```bash
-arduino-cli core install STMicroelectronics:stm32
-arduino-cli lib install "Blues Wireless Notecard" ModbusMaster OneWire DallasTemperature
-arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/solar_string_monitor/solar_string_monitor.ino
-arduino-cli upload -p /dev/ttyACM0 --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/solar_string_monitor/solar_string_monitor.ino
-```
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "solar_alert.qo",
- "body": {
- "string_id": 3,
- "reason": "shading",
- "perf_ratio": 0.63,
- "actual_w": 2989.8,
- "expected_w": 4766.5,
- "string_v": 402.4,
- "string_a": 7.43,
- "irradiance_wm2": 882.1,
- "mod_temp_c": 53.4
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with onboard Cygnet STM32L433 host — exposes UART TX/RX and A0–A5 analog inputs needed for this project, no external MCU required. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular connectivity removes per-site WiFi/IT dependencies; prepaid SIM included. |
-| [Taoglas GA.111.101111 LTE Magnetic Mount Antenna, 3 dBi, 1 m SMA](https://www.taoglas.com/product/ga-111-101111-3dbi-mini-magnetic-mount-698960mhz-17102700mhz/) | 1 | Omnidirectional LTE/3G/2G antenna covering 698–2700 MHz; strong neodymium magnet base mounts directly on the metal NEMA 4X enclosure lid with no additional hardware. 1 m RG-174 cable terminates in SMA male. Placing the antenna outside the metal enclosure is mandatory for reliable LTE Cat-1 bis reception — internal antennas adjacent to inverter metalwork will not work reliably. |
-| [SparkFun WRL-18568 SMA to U.FL Cable, 150 mm](https://www.sparkfun.com/sma-to-u-fl-cable-150mm.html) | 1 | Pigtail that bridges the Notecarrier CX U.FL cellular antenna port to the GA.111's SMA male connector inside the enclosure. The GA.111 cable enters through a weatherproof cable gland; the 150 mm pigtail is sufficient to reach from the M.2 slot area to the gland entry point. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter on the power rail for bench-validation of the sleep/wake power profile. |
-| [SparkFun RS-485 Transceiver Breakout (BOB-10124)](https://www.sparkfun.com/products/10124) | 1 | Converts Cygnet UART to RS-485 half-duplex differential signal; SP3485 chip runs on single 3.3V supply and is 5V-logic tolerant. Connects UART TX/RX and a direction-control GPIO (D9) to the RS-485 A/B bus. |
-| [Apogee SP-110-SS Self-Powered Pyranometer](https://www.apogeeinstruments.com/sp-110-ss-self-powered-pyranometer/) | 1 | Class-B silicon-cell pyranometer with 0–400 mV analog output (0.2 mV per W/m²). Self-powered — no excitation voltage needed. Designed for solar resource monitoring and PV system performance analysis. |
-| [Adafruit Waterproof DS18B20 Temperature Sensor (Product 381)](https://www.adafruit.com/product/381) | 1 | 1-Wire digital probe in a stainless-steel housing; ±0.5 °C from −10 °C to +85 °C; rated to 125 °C which covers panel backsheet temperatures in hot climates. Attached to a representative panel backsheet with thermal adhesive tape. |
-| 4.7 kΩ resistor | 1 | 1-Wire pull-up from DS18B20 data line to 3V3. |
-| 120 Ω resistor | 2 | RS-485 bus termination at each end of the cable (one at the Notecarrier CX breakout, one at the combiner/inverter). |
-| Shielded twisted pair, 22 AWG, RS-485-rated | 1 run | A → A, B → B wire pairs; shield bonded at the combiner end to chassis earth only (single-ended bond). If the inverter requires a dedicated COM/signal-ground conductor, add a third wire alongside the A/B pair (see Wiring and Assembly). Length depends on enclosure placement. |
-| AC/DC supply, 5V/2A (e.g. [MeanWell IRM-10-5](https://www.meanwell.com/Upload/PDF/IRM-10/IRM-10-SPEC.PDF)) | 1 | Derives 5V DC from the array's local AC supply. The IRM-10-5 is encapsulated, rated for wide-range AC input (85–264 VAC), and delivers up to 2A (10W) — providing adequate headroom for the Notecard Cell+WiFi cellular burst current, the Cygnet host, and the RS-485 transceiver running simultaneously. |
-| Metal NEMA 4X enclosure, ~6×4×3″ | 1 | Weather-rated metal housing for outdoor installation near the combiner or on a rooftop equipment pad. A metal lid is required for the magnetic-base antenna mount (Taoglas GA.111); a non-metal enclosure would need an alternative antenna solution such as an external bracket or mast mount. |
-
-All Blues hardware ships with a prepaid SIM including 500 MB of data and 10 years of service: no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-
-
-⚠️ **Electrical Safety — Qualified Personnel Required**
-Solar PV arrays and associated combiner/inverter equipment involve hazardous DC voltages (typically 300–1000 V on the string bus) and AC mains. All installation, wiring, and commissioning work must be performed by a licensed electrician or qualified PV installer in accordance with:
-- Applicable national and local electrical codes (NEC Article 690, IEC 62548, or jurisdiction equivalent)
-- Inverter and combiner manufacturer installation instructions
-- Lockout/tagout (LOTO) procedures before working near or on energized PV conductors
-
-Do not work on wiring connected to the PV array, combiner, or AC supply without confirmed isolation.
-
-
-
-Inside the NEMA 4X enclosure, everything ties back to the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) and its dual 16-pin header. The Notecard Cell+WiFi seats into the M.2 slot and talks to the Cygnet host over the on-board I²C bus, so no wiring is needed between them. During bench validation, the Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad as a power monitor, with a Qwiic cable running from one of the Mojo's Qwiic ports to the Notecarrier CX Qwiic connector. That gives the Notecard a data path to read Mojo's coulomb counter. See the Mojo subsection below and [§9](#9-validation-and-testing) for the bench readout procedure.
-
-**RS-485 transceiver (SparkFun BOB-10124 → Modbus combiner):**
-
-- **TX** (Notecarrier CX) → **DI** (BOB-10124): UART transmit from Cygnet to RS-485 driver input.
-- **RX** (Notecarrier CX) → **RO** (BOB-10124): RS-485 receiver output back to Cygnet.
-- **D9** (Notecarrier CX) → **DE** and **/RE** tied together (BOB-10124): Direction control — HIGH = transmit, LOW = receive (half-duplex). Tie DE and /RE to the same net.
-- **+3V3** (Notecarrier CX) → **VCC** (BOB-10124): The SP3485 runs on 3.3V; do not connect to 5V.
-- **GND** → **GND** (BOB-10124).
-- **A** (BOB-10124) → combiner/inverter **A (+)** terminal.
-- **B** (BOB-10124) → combiner/inverter **B (−)** terminal.
-- **Cable shield** → combiner/inverter chassis earth at the combiner end only (single-ended shield bond prevents ground loops). Do **not** ground the shield at both ends.
-- **RS-485 signal ground (COM).** Some inverters require a dedicated third conductor — often labeled **C**, **COM**, or **GND** on the RS-485 port — in addition to cable shield bonding, to establish a common voltage reference between the two RS-485 nodes. Check the inverter's Modbus wiring diagram: if a COM terminal is shown, connect a third wire from the BOB-10124 **GND** pin to that terminal alongside the A/B pair. Without it the bus can work at short distances but is prone to noise errors on longer cable runs.
-- Place a 120 Ω resistor across A/B at the BOB-10124 end of the cable and a second 120 Ω resistor at the combiner's RS-485 port, as required for RS-485 bus termination.
-
-**Pyranometer (Apogee SP-110-SS → A0):**
-
-- SP-110-SS **positive (white)** wire → **A0** (Notecarrier CX).
-- SP-110-SS **negative (black)** wire → **GND**.
-- Mount the pyranometer co-planar with the panels (same tilt and azimuth) within 2–3 meters of the array. Keep it away from shading objects and out of panel shadow.
-
-**Module temperature probe (Adafruit DS18B20 → D5):**
-
-- DS18B20 **data (yellow)** → **D5** (Notecarrier CX).
-- DS18B20 **power (red)** → **+3V3** (Notecarrier CX). The DS18B20 supports parasitic power from the data line, but external 3V3 supply eliminates timing sensitivity.
-- DS18B20 **ground (black)** → **GND**.
-- Place a **4.7 kΩ** resistor from D5 to +3V3 (1-Wire pull-up, mandatory).
-- Attach the probe tip to the backsheet of a representative module with thermal adhesive tape, mid-string, away from frame edges and bypass diodes.
-
-**Cellular antenna (Taoglas GA.111 + SparkFun WRL-18568):**
-
-- Seat the WRL-18568 U.FL end onto the Notecarrier CX U.FL cellular antenna port.
-- Pass the GA.111's 1 m cable through a weatherproof cable gland in the enclosure wall, then screw the GA.111 SMA male connector onto the WRL-18568 SMA female end inside the enclosure.
-- Set the GA.111 magnetic base on the exterior of the metal enclosure lid, away from the inverter's switching power supply and transformer — at least 30 cm separation is recommended. The neodymium magnet holds the antenna securely on the metal surface without additional hardware.
-- Avoid routing the antenna coax parallel to AC power cables inside the enclosure; cross AC conductors at 90° if they must share the same path.
-
-**Sleep wiring:**
-
-No external jumper is required. The Notecarrier CX gates the Cygnet host's 3.3 V rail internally using the Notecard's ATTN signal. `NotePayloadSaveAndSleep` issues a [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) sleep request; the CX board circuitry uses the resulting ATTN assertion to cut and later restore the Cygnet's supply without any user-added wire.
-
-**Power chain:**
-
-- AC mains → MeanWell IRM-10-5 (5V/2A output) → Mojo **BAT** input → Mojo **LOAD** output → Notecarrier CX **+VBAT** pad.
-
-**Mojo data connection (bench validation only):**
-
-- The power path above places Mojo inline on the 5V rail; no additional power wiring is required.
-- Mojo **Qwiic** port → Notecarrier CX **Qwiic** connector (standard 100 mm Qwiic cable). The Notecard reads Mojo's coulomb counter / power monitor over this Qwiic connection and exposes live power-consumption data via [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power). This is the data path used to validate the sleep/wake power profile at the bench.
-- **Reading Mojo data.** With the assembly powered exclusively through +VBAT (USB cable to Notecarrier disconnected — see §9 for why USB must be absent), open the [In-Browser Terminal](https://dev.blues.io/terminal/) and send `{"req":"card.power"}` to query Mojo live. To enable periodic energy logging to Notehub, send `{"req":"card.power","minutes":60}` — the Notecard will then write a power-consumption snapshot to `_log.qo` at that interval, which you can export from Notehub as CSV for full-session energy analysis. The application firmware does not call `card.power` on each sample cycle; Mojo measurements are bench instrumentation only (see [§11 Limitations](#11-limitations-and-next-steps)).
-- **Before field deployment:** remove the Qwiic cable. Mojo itself stays on the bench; only the IRM-10-5 → Notecarrier CX +VBAT power path is present in the enclosure.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) and paste it into `firmware/solar_string_monitor/solar_string_monitor.ino` as `PRODUCT_UID`.
-2. **Claim the Notecard.** Power the unit; on first cellular session the Notecard associates with your project automatically.
-3. **Create a Fleet per site.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration and routing. The natural unit is one fleet per installation: every device on the same site reads the same inverter model with the same register map and the same string rating. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) let you promote a device to a different fleet dynamically (e.g., a replacement unit with a different panel model) without re-flashing.
-4. **Set environment variables.** All variables below are optional; firmware defaults apply if not set. Any variable set in Notehub overrides the compile-time default on the device's next inbound sync; operators can retune thresholds and register addresses across an entire fleet without touching the firmware.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_sec` | `300` | Seconds between Modbus + sensor samples. |
- | `report_interval_min` | `60` | Minutes between summary Notes. |
- | `modbus_slave_id` | `1` | Modbus server (slave) address of the string combiner or inverter. |
- | `modbus_baud` | `9600` | RS-485 bus baud rate. Must match the inverter configuration. Common values: 9600, 19200. See `modbus_parity` and `modbus_stop_bits` for complete serial-framing control. |
- | `modbus_parity` | `"none"` | RS-485 framing parity: `"none"`, `"even"`, or `"odd"`. Must match the inverter's Modbus port configuration. Default `"none"` → 8N1 or 8N2 depending on `modbus_stop_bits`. Changes take effect on the next device wake with no firmware reflash required. |
- | `modbus_stop_bits` | `1` | RS-485 stop bits per character: `1` or `2`. Most Modbus RTU devices use 1 stop bit; some older meters and energy analyzers require 2. Changes take effect on the next device wake. |
- | `n_strings` | `4` | Number of strings to poll (1–4). A single-string deployment (`n_strings = 1`) still fires PR-threshold alerts, but the root-cause hypothesis always emits as `degraded` — shading/soiling/string_fault classification requires at least two peer strings to compare against. |
- | `reg_base` | `100` | 0-based holding-register address of string 1's voltage register. The firmware reads `2×n_strings` contiguous registers: [V1, I1, V2, I2, …]. |
- | `string_v_scale_x100` | `10` | Voltage scaling factor × 100. Raw register value × (this / 100) = volts. Default 10 → 0.10 V/count. |
- | `string_a_scale_x1000` | `10` | Current scaling factor × 1000. Raw register value × (this / 1000) = amps. Default 10 → 0.010 A/count. |
- | `string_stc_w` | `6000` | Rated power per string at STC (W). Used to compute expected power. Default 6000 W is representative of a 15-module × 400 W C&I string (~600 V, ~10 A at STC); update to match the actual installed string rating before or immediately after commissioning. |
- | `perf_thresh_pct` | `80` | Alert when PR < this value / 100. Default 80 → alert when PR < 0.80. |
- | `irradiance_min_wm2` | `100` | Minimum irradiance (W/m²) required before performance is evaluated. Suppresses false alerts at dawn, dusk, and overcast conditions where noise dominates. |
- | `temp_coeff_per10000` | `-35` | Module power temperature coefficient × 10 000. Default −35 → −0.0035 /°C (typical for mono-PERC silicon). Adjust for the actual module datasheet value. |
- | `alert_cooldown_sec` | `1800` | Minimum seconds between repeat alerts for the same string (default 30 minutes). Converted to a sample count at runtime so the wall-clock window stays consistent even when `sample_interval_sec` is changed. |
- | `pyranometer_mv_per_wm2_x1000` | `200` | Pyranometer sensitivity × 1000 in mV/(W/m²). Default 200 → 0.200 mV/(W/m²) (Apogee SP-110-SS nominal). Replace with the per-unit calibration certificate value to correct unit-to-unit variation. |
-
- > **Register-map Note.** The `reg_base` default and scaling defaults are illustrative for a demo register layout. The firmware reads contiguous pairs `[V1, I1, V2, I2, …]` — one voltage and one current register per string, and requires that the monitored device provides an independent operating voltage for each string. Multi-MPPT inverters implementing [SunSpec Model 160 Multiple MPPT](https://sunspec.org/sunspec-modbus-specifications/) expose per-MPPT voltage (DCV) and current (DCA) in this form; traditional string combiners typically do not. Production deployments must consult the inverter's Modbus communication manual for actual register addresses, addressing convention (0-based wire-level vs. 1-based / Modicon "40001" notation), scaling, and signedness. See [Limitations](#11-limitations-and-next-steps).
-
- > **Timing constraint.** For each summary window to cover exactly the configured report period, `report_interval_min × 60` should be an integer multiple of `sample_interval_sec` (e.g., the defaults 60 minutes and 300 seconds satisfy 3600 / 300 = 12 exactly). If it is not, the firmware uses ceiling division when computing the window length, so the actual window covers at least the full configured period with one additional sample rather than falling short. The `n_samples` field in each summary Note reflects the actual sample count used; with a non-divisible configuration it will be `⌈report_interval_min × 60 / sample_interval_sec⌉` rather than the exact quotient.
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `solar_alert.qo` (real-time delivery to an O&M on-call system or CMMS) and one for `solar_summary.qo` (batched delivery to a long-term analytics store). Separating the two Notefiles at the source means each can be fanned out to a different destination at a different urgency without any filtering logic in the route.
-
-## 7. Firmware Design
-
-The firmware is split between a main sketch — [`firmware/solar_string_monitor/solar_string_monitor.ino`](firmware/solar_string_monitor/solar_string_monitor.ino) — and a pair of helper files for the sensor and Notecard data path: [`firmware/solar_string_monitor/solar_string_monitor_helpers.cpp`](firmware/solar_string_monitor/solar_string_monitor_helpers.cpp) and [`firmware/solar_string_monitor/solar_string_monitor_helpers.h`](firmware/solar_string_monitor/solar_string_monitor_helpers.h). The split keeps the orchestration in `.ino` readable while the Modbus, sensor, and template logic lives separately.
-
-**Dependencies** (install via Arduino Library Manager or `arduino-cli lib install`):
-- **Arduino core for STM32** (`stm32duino/Arduino_Core_STM32`) — supports the Cygnet STM32L433.
-- [**Blues Wireless Notecard**](https://github.com/blues/note-arduino) (`note-arduino`) — the `note-arduino` library.
-- [**ModbusMaster**](https://github.com/4-20ma/ModbusMaster) by 4-20ma — portable Modbus RTU client that works with any Arduino `Stream`-compatible serial port, including `Serial1` on the Cygnet.
-- [**OneWire**](https://www.pjrc.com/teensy/td_libs_OneWire.html) and [**DallasTemperature**](https://github.com/milesburton/Arduino-Temperature-Control-Library) — DS18B20 1-Wire driver.
-
-### Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard configuration (`hub.set`, accelerometer off, template definitions) | `setup()` in `.ino` → `defineTemplates()` in helpers |
-| Environment-variable fetch and clamp | `fetchEnvVars()` in helpers |
-| RS-485 direction control | `preTransmission()` / `postTransmission()` callbacks in `.ino` |
-| Modbus serial framing (parity / stop-bits) | `serialConfigFromEnv()` in helpers → `Serial1.begin()` in `setup()` |
-| Pyranometer read | `readIrradiance()` in helpers |
-| DS18B20 module temperature read | `readModuleTemp()` in helpers |
-| Modbus RTU string poll | `readStrings()` in helpers |
-| Temperature-derated expected power | `computeExpected()` in helpers |
-| Per-string running accumulators | `accumulateWindow()` in helpers |
-| PR evaluation, hypothesis, and alert emission | `evaluateAndAlert()` in helpers |
-| Hourly summary Note | `sendSummary()` in helpers |
-| Sleep and state persistence | `loop()` in `.ino` → `NotePayloadSaveAndSleep` |
-
-### Sensor reading strategy
-
-**Pyranometer (Apogee SP-110-SS).** The sensor outputs 0.2 mV per W/m² directly onto A0. The firmware takes an 8-sample average to reduce ADC noise, converts counts to millivolts at the Cygnet's 3.3V / 12-bit reference, then divides by 0.2 to get W/m². At 1000 W/m² (STC) the sensor outputs approximately 200 mV, which lands at roughly 248 ADC counts — adequate resolution for threshold comparisons. Adding a 10× non-inverting op-amp amplifier between the sensor and A0 would improve effective resolution to about 11 bits, which is recommended for production deployments where irradiance measurement quality matters for precise PR calculation.
-
-**DS18B20 module temperature.** 1-Wire `requestTemperatures()` + `getTempCByIndex(0)` at 11-bit resolution (375 milliseconds conversion time). On an invalid reading — disconnected probe (`DEVICE_DISCONNECTED_C`) or out-of-range value (< −40 °C or > 110 °C) — the firmware returns a `−9999` sentinel and emits a rate-limited `temp_probe_fault` alert Note (`sync:true`, at most once per report window). The DS18B20's 85 °C "power-on sentinel" is *not* in the validity checks because `requestTemperatures()` blocks for the full conversion time, so the scratchpad always holds a real measurement before the read; treating 85 °C as a fault would falsely reject genuine backsheet readings near 85 °C, which is a realistic value on a hot-rooftop array in summer. PR evaluation and per-string window accumulation are suppressed for any sample cycle the probe returns the `−9999` sentinel so no fabricated expected-power values reach the accumulators or Notehub. `mod_temp_c` emits `−9999` in the summary for any window containing no valid temperature readings, allowing downstream consumers to distinguish a sensor failure from a real near-zero temperature measurement.
-
-**Modbus string reads.** A single `readHoldingRegisters(base, 2×n)` call pulls all strings' voltage and current registers in one Modbus transaction, which is roughly 8× more bus-efficient than individual per-register reads. The registers are expected in contiguous pairs `[V1, I1, V2, I2, …]` starting at `reg_base`. Real inverters vary; see [Limitations](#11-limitations-and-next-steps).
-
-### Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Templates store Notes as fixed-length binary records rather than free-form JSON, reducing per-Note wire size by 3–5×, which is meaningful at 24 summary Notes/day per device over a multi-year deployment on a shared SIM.
-
-**Alert Note** (`solar_alert.qo`, `sync:true`, fires on Performance Ratio threshold trip):
-
-```json
-{
- "file": "solar_alert.qo",
- "body": {
- "string_id": 3,
- "reason": "shading",
- "perf_ratio": 0.63,
- "actual_w": 2989.8,
- "expected_w": 4766.5,
- "string_v": 402.4,
- "string_a": 7.43,
- "irradiance_wm2": 882.1,
- "mod_temp_c": 53.4
- },
- "sync": true
-}
-```
-
-Field reference:
-- `string_id`: 1–4 (which string underperformed); 0 = Modbus bus failure
-- `reason`: `"shading"` (low V, normal I), `"soiling"` (normal V, low I), `"string_fault"` (both low), `"degraded"` (unclear pattern or single-string mode), `"temp_probe_fault"`, `"modbus_fail"`
-- `perf_ratio`: actual power / temperature-derated expected power
-- `actual_w`: measured string power at that moment
-- `expected_w`: STC power × (irradiance / 1000) × (1 + temp_coeff × (temp − 25)) — the derated STC expectation
-- Rate-limited: repeat alerts for the same string are suppressed for 30 minutes (configurable `alert_cooldown_sec`)
-
-**Summary Note** (`solar_summary.qo`, sent hourly):
-
-```json
-{
- "file": "solar_summary.qo",
- "body": {
- "irradiance_wm2": 867.2,
- "mod_temp_c": 52.1,
- "s1_v": 596.4, "s1_a": 8.03, "s1_w": 4789.1, "s1_ew": 4710.8, "s1_pr": 1.02,
- "s2_v": 598.1, "s2_a": 8.01, "s2_w": 4790.8, "s2_ew": 4710.8, "s2_pr": 1.02,
- "s3_v": 401.3, "s3_a": 7.41, "s3_w": 2973.6, "s3_ew": 4710.8, "s3_pr": 0.63,
- "s4_v": 597.8, "s4_a": 8.02, "s4_w": 4794.4, "s4_ew": 4710.8, "s4_pr": 1.02,
- "alert_flags": 4,
- "n_samples": 12
- }
-}
-```
-
-Field reference:
-- `irradiance_wm2`, `mod_temp_c`: window means across all samples (including Modbus failures), so environmental context is always complete
-- `sN_v`, `sN_a`: voltage and current means over Modbus-valid samples only; `−9999` if no valid reads
-- `sN_w`: actual power mean (V × I)
-- `sN_ew`: temperature-derated expected power mean
-- `sN_pr`: performance ratio mean (sN_w / sN_ew)
-- `alert_flags`: bitmask (bit 0 = string 1, bit 1 = string 2, etc.); `4` = binary 0100 = string 3 is currently flagged
-- `n_samples`: count of sample cycles in the window; default is 12 (60 minutes ÷ 5 minutes). If `report_interval_min × 60` is not evenly divisible by `sample_interval_sec`, the window extends by one sample (ceiling division)
-
-### Low-power strategy
-
-The Cygnet host wakes, runs one complete sample cycle (typically 2–5 seconds of active computation and I²C), and then calls `NotePayloadSaveAndSleep`, which serializes `g_state` into Notecard flash and issues [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to cut Cygnet host power for `g_sample_interval_sec` seconds. The Notecard itself idles at ~8–18 µA between cellular syncs. Sampling and transmission cadences are deliberately decoupled: the device samples every 5 minutes but only connects to Notehub once an hour. Alerts are the only thing that bypass the hourly transmit timer.
-
-### Sync strategy
-
-The Notecard is configured in [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `periodic` mode: it does not hold a persistent cellular connection, but instead wakes the radio on a scheduled cadence. `outbound` is set to `g_report_interval_min` (default 60 minutes), so queued summary Notes are flushed to Notehub roughly once an hour. `inbound` is fixed at 120 minutes: every two hours the Notecard checks Notehub for fleet-level [environment variable](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) updates. This means a threshold or register-map change published in Notehub can take up to two hours to propagate to a deployed device; plan for that lag when tuning a running installation. If `report_interval_min` is changed via a Notehub environment variable, the firmware re-issues `hub.set` on the next wake cycle so the Notecard's outbound sync cadence stays aligned with the new summary period without a firmware reflash.
-
-Alert Notes carry `sync:true`, which bypasses the scheduled `outbound` window entirely: the Notecard wakes the radio immediately on receiving a `sync:true` Note regardless of the outbound timer. This ensures alert latency of roughly 15–60 seconds to Notehub while keeping the bulk of traffic in the efficient hourly batch. Connecting every 5 minutes for each summary sample would consume far more energy than accumulating 12 samples and flushing them in a single session.
-
-### Retry and error handling
-
-- The first Notecard transaction in `setup()` is the cold-boot `hub.set`. The firmware sends it with `sendRequestWithRetry` (5-second timeout) to handle the I²C readiness race on cold boot; if all retry attempts fail, the boolean return is `false`, the error is logged, and `last_hub_outbound` is left at 0. On the next wake, the cadence-mismatch check (which uses `requestAndResponse` and inspects the Notecard `err` field) re-issues `hub.set`, so no device is silently left unconfigured.
-- `readStrings()` retries the Modbus read up to 3× before failing. On a complete failure it emits a `modbus_fail` alert Note (rate-limited to once per report window) rather than silently dropping the sample, so the O&M operator can tell the difference between "the array is down" and "the monitoring device lost Modbus".
-- Environment variable values from Notehub are clamped to physically reasonable ranges in `fetchEnvVars()`, so a typo in the Notehub UI can't drive `g_perf_threshold` to a value that fires every sample or never fires at all.
-- Alert de-duplication uses a configurable `alert_cooldown_sec` window (default 1800 seconds = 30 minutes). The sample-count equivalent is computed at runtime — `⌈alert_cooldown_sec / sample_interval_sec⌉`, so the 30-minute wall-clock window holds even if `sample_interval_sec` is changed via a Notehub env var.
-
-### Key code snippet 1: Performance Ratio and root-cause hypothesis
-
-The firmware computes PR for each string, then examines the operating voltage and current signature at the inverter/combiner measurement point relative to the fleet mean to choose a hypothesis. These are live operating-point readings under load, not open-circuit or short-circuit values. Low operating string voltage with near-normal operating current indicates partial shading; near-normal voltage with low operating current indicates soiling; both low indicates a more serious string fault.
-
-Comparative classification requires at least two peer strings. When `n_strings = 1` there is no fleet mean to compare against (the mean equals the single string's own values), so the hypothesis block is skipped and every PR alert fires as `degraded`. See [§11 Limitations](#11-limitations-and-next-steps) for details on single-string and mixed-geometry deployments.
-
-```cpp
-float pr = (exp_w > 0.1f) ? (v[i] * a[i]) / exp_w : 0.0f;
-
-// "degraded" is the catch-all; comparative hypotheses require n >= 2.
-const char *reason = "degraded";
-if (n >= 2) {
- float vr = (v_mean > 0.1f) ? v[i] / v_mean : 1.0f;
- float ar = (a_mean > 0.1f) ? a[i] / a_mean : 1.0f;
- if (vr < 0.85f && ar >= 0.90f) reason = "shading";
- else if (ar < 0.85f && vr >= 0.90f) reason = "soiling";
- else if (vr < 0.85f && ar < 0.85f) reason = "string_fault";
-}
-```
-
-### Key code snippet 2: temperature-derated expected power model
-
-The expected power adjusts for the difference between the current module temperature and the STC reference of 25 °C. A mono-PERC silicon module at 65 °C (a common rooftop summer temperature) loses roughly 14% of its STC rating from temperature alone; the model accounts for this before comparing actual to expected.
-
-```cpp
-float temp_factor = 1.0f + g_temp_coeff * (mod_temp_c - 25.0f);
-return g_string_stc_w * (irr_wm2 / 1000.0f) * temp_factor;
-```
-
-### Key code snippet 3: immediate-sync alert
-
-`sync:true` bypasses the periodic `outbound` window. The Notecard wakes the radio immediately; alert latency from the event to Notehub is typically 15–60 seconds.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", ALERT_NOTEFILE);
-JAddBoolToObject(req, "sync", true);
-J *b = JAddObjectToObject(req, "body");
-JAddNumberToObject(b, "string_id", str_id);
-JAddStringToObject(b, "reason", reason);
-JAddNumberToObject(b, "perf_ratio", pr);
-notecard.sendRequest(req);
-```
-
-### Key code snippet 4: sleep with state persistence
-
-State survives the power cut because it's stored in Notecard flash, not the Cygnet's RAM. On the next wake, `setup()` calls `NotePayloadRetrieveAfterSleep` to restore everything — sample count, accumulators, alert state — before `loop()` runs again.
-
-```cpp
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, kSeg, &g_state, sizeof(g_state));
-NotePayloadSaveAndSleep(&payload, g_sample_interval_sec, NULL);
-```
-
-## 8. Data Flow
-
-
-
-**Collected every `sample_interval_sec` (default 5 minutes):** DC voltage and current for each configured string (from Modbus), irradiance in W/m² (from pyranometer ADC), and module backsheet temperature in °C (from DS18B20).
-
-**Computed locally:** expected power per string (irradiance-normalized and temperature-derated), performance ratio per string, and root-cause hypothesis for any string below threshold.
-
-**Transmitted:**
-- `solar_summary.qo` — once per `report_interval_min` (default 24 Notes/day), template-encoded, queued and shipped by the Notecard's outbound cellular sync. `irradiance_wm2` and `mod_temp_c` are window means across all sample cycles; per-string fields are means over Modbus-valid samples in the window. Strings with zero valid Modbus reads in the window emit `−9999`. `sample_interval_sec` and `report_interval_min` can be tuned independently, but for the window to cover exactly the configured report period `report_interval_min × 60` must be an integer multiple of `sample_interval_sec`; if it is not, the firmware rounds the window length up (see §6 Timing constraint Note).
-- `solar_alert.qo` — emitted immediately on a threshold trip, `sync:true`, with a per-string de-duplication window (default 30 minutes, tunable via `alert_cooldown_sec`). A `modbus_fail` alert (string_id=0) fires when the RS-485 bus is unreachable and is rate-limited to once per report window. The `alert_flags` bitmask in `solar_summary.qo` reflects the **last known active state** of each string. Flags are cleared unconditionally whenever irradiance drops below `irradiance_min_wm2` — this happens at the loop level, independent of Modbus or probe success, so overnight and low-light summaries always report 0 rather than carrying forward stale daytime fault states even if telemetry happened to drop out at sunset. When irradiance is above the threshold, flags are updated only on sample cycles where Modbus polling succeeds and the temperature probe is valid; if telemetry has dropped out across multiple cycles within a daytime window, the flags carry forward the state from the most recent successful poll.
-
-**Routed.** Both Notefiles reach Notehub and from there are fanned out to whatever downstream the project's routes specify. The O&M operator typically wants `solar_alert.qo` on their existing on-call channel (SMS, CMMS ticket, Slack) and `solar_summary.qo` in a long-term store for energy yield analysis.
-
-**Alert triggers:**
-
-
-
-**Single-string Note.** When `n_strings = 1`, comparative root-cause classification (`shading`, `soiling`, `string_fault`) is unavailable — there are no peer strings to compare against. All PR-threshold trips fire as `degraded`. This is correct, documented behavior; see [§11 Limitations](#11-limitations-and-next-steps).
-
-
-
-- `shading` — string PR below threshold with operating voltage significantly lower than fleet mean but operating current near normal. Classic partial-shade signature: shadow across one or more cells reduces operating string voltage while current in the bypass-diode conduction path is less affected. Requires `n_strings ≥ 2`.
-- `soiling` — string PR below threshold with operating current significantly lower than fleet mean but voltage near normal. Uniform soiling (dust, pollen, bird droppings) reduces operating current proportionally across all cells in the string; voltage drops only slightly. Requires `n_strings ≥ 2`.
-- `string_fault` — both voltage and current significantly below fleet mean. Indicates a serious fault: broken cell, failed bypass diode, high series resistance, or a physical disconnection. Requires `n_strings ≥ 2`.
-- `degraded` — PR below threshold but V/I signature doesn't fit the above patterns clearly; catch-all for mixed or unclear signatures, and the only hypothesis emitted when `n_strings = 1`.
-- `temp_probe_fault` — DS18B20 backsheet probe returned an invalid reading (disconnected or out-of-range, i.e. < −40 °C or > 110 °C); rate-limited to once per report window. PR evaluation is suppressed for the affected sample cycle; `mod_temp_c` emits `−9999` in any summary window with no valid temperature readings.
-- `modbus_fail` — Modbus bus unreachable after 3 retries; the whole combiner or the RS-485 cable is suspect.
-
-## 9. Validation and Testing
-
-**Expected cadence.** In steady state on a sunny day a correctly-behaving array generates one `solar_summary.qo` event per `report_interval_min` minutes (default: one per hour) and zero `solar_alert.qo` events. Tuning `report_interval_min` down via Notehub automatically re-issues `hub.set` on the next wake cycle so the Notecard's outbound sync cadence stays aligned with the new summary interval. When changing either `report_interval_min` or `sample_interval_sec`, verify that `report_interval_min × 60` remains an integer multiple of `sample_interval_sec`; if it is not, the firmware rounds the window length up so the actual summary window covers at least the full configured period (see §6 Timing constraint Note). At night or under heavy overcast, irradiance falls below `irradiance_min_wm2` and PR evaluation is suppressed; the summary Note still emits, carrying the actual (near-zero) measured values.
-
-**Modbus first-light.** Before connecting to the real inverter, run the firmware against a USB-RS-485 adapter and a Modbus simulator (Modbus Mechanic, ModRSsim2, or any slave simulator) to verify the register reads match the expected values. Confirm that `modbus_baud`, `modbus_slave_id`, `modbus_parity`, and `modbus_stop_bits` match the inverter configuration — all four are settable via Notehub [environment variables](#6-notehub-setup) without re-flashing. Default framing is 8N1 (no parity, 1 stop bit), which matches most Modbus RTU devices. Most field-commissioning problems trace to a single parameter mismatch.
-
-**Alert simulation.** The easiest way to force alerts without going to the roof: in the Fleet environment variables, set `string_stc_w` to a value four or more times higher than the actual string's STC power rating (for example, change the default `6000` to `24000`). The inflated expected-power denominator drives every configured string's computed PR well below the 80% default threshold. After the next inbound sync delivers the change (up to 120 minutes. See the Sync strategy Note in §7), all strings will fire `degraded` alerts on their next sample. Confirm that `solar_alert.qo` events appear in Notehub for each configured string. Restore `string_stc_w` to the correct rated value when done.
-
-
-
-Do not use `perf_thresh_pct` values above `100` for this test; the firmware clamps that variable to a maximum of `100` (effective threshold: PR < 1.00), so setting `120` produces no additional effect beyond `100`.
-
-
-
-**Using Mojo to validate the power profile.** The table below separates Notecard-only figures drawn from the published datasheet from the whole-assembly behavior observed at the +VBAT pad. +VBAT measurements include the Notecarrier regulator, transceiver, and any peripheral quiescent draw in addition to the Notecard itself, so they will not equal the bare-Notecard datasheet values. Use the Mojo to characterize the actual idle and burst figures for your specific stack.
-
-| State | Notecard MBGLW (datasheet) | Whole-assembly at +VBAT |
-|---|---|---|
-| Idle: Cygnet off, Notecard between syncs | ~8–18 µA @ 5V — [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) idle figure (range reflects variation across MBGLW hardware revisions) | Notecard-dominated during sleep; `NotePayloadSaveAndSleep` causes the Notecarrier CX to cut the Cygnet's 3.3 V rail, so the RS-485 transceiver, DS18B20, and Cygnet are all off. The whole-assembly +VBAT idle floor exceeds the bare-Notecard figure by the Notecarrier regulator overhead only. Measure with the Mojo on your specific stack to establish the actual baseline. |
-| Host awake + sampling (~2–5 s burst, every 5 min) | Radio off; Notecard is actively servicing I²C traffic from the Cygnet host during this phase. No published datasheet figure exists for this transient operating state — measure whole-system current at +VBAT with the Mojo. | Cygnet core, RS-485 driver, pyranometer ADC, and 1-Wire transceiver all draw current during this burst; the whole-assembly +VBAT burst current is Cygnet-dominated and will substantially exceed the bare-Notecard sleep floor. Measure with the Mojo to characterize the actual burst current for your stack. |
-| Cellular sync (hourly, ~15–60 s per session) | 100–300 mA average; peak bursts during radio warm-up, network registration, and TLS handshake — see the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) and the [low-power firmware design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for characterized per-session energy figures | Notecard-dominated; Cygnet is off during the sync window |
-
-Connect [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline in the power path (MeanWell 5V output → Mojo BAT, Mojo LOAD → Notecarrier CX +VBAT) and connect a Qwiic cable from Mojo to the Notecarrier CX Qwiic port so the Notecard can read the coulomb counter. With USB disconnected from the Notecarrier (mandatory — see below), open the [In-Browser Terminal](https://dev.blues.io/terminal/) and send `{"req":"card.power"}` to query Mojo live. To enable periodic energy logging, send `{"req":"card.power","minutes":60}` — the Notecard will write a power-consumption snapshot to `_log.qo` at that interval, which you can export from Notehub as CSV for full-session energy analysis.
-
-With the default cadence (5-minute samples, hourly sync), a healthy trace shows three distinct current levels: a low idle floor between sample cycles (Notecard-dominated; the +VBAT reading will exceed the bare-Notecard ~8–18 µA figure by the Notecarrier regulator overhead — `NotePayloadSaveAndSleep` cuts the Cygnet's 3.3 V rail during sleep, so the RS-485 transceiver, DS18B20, and Cygnet do not contribute to the idle current; measure your specific stack to establish the actual baseline); brief 2–5 s active bursts every five minutes when the Cygnet wakes, reads sensors, computes PR, hands off to the Notecard, and sleeps again; and one ~15–60 s cellular excursion per hour for the outbound sync, in the range characterized by the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/). Alert notes (`sync:true`) produce an additional out-of-schedule cellular burst whenever a PR threshold trips.
-
-**Measurement conditions for sleep-current validation.** Approaching the low-idle regime described by the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) requires measuring with **USB/VUSB absent** — VUSB present on the Notecarrier holds the Notecard out of its lowest-power state regardless of what the firmware does. Power the assembly exclusively through the Mojo on the +VBAT pad, with no USB cable attached to the Notecarrier. The measured +VBAT idle floor will exceed the bare-Notecard datasheet figure by the Notecarrier regulator overhead; `NotePayloadSaveAndSleep` cuts the Cygnet's 3.3 V rail during sleep, so the RS-485 transceiver, DS18B20, and Cygnet do not contribute. This is expected and normal. If the idle floor is still consistently above 1 mA after removing USB, `NotePayloadSaveAndSleep` is not reaching its sleep call — confirm the sleep path executes on every sample cycle without returning early. As a secondary check, confirm the Notecarrier CX DIP switch is set to `HST` rather than `NC`; `HST` is the correct operating position and eliminates one potential source of spurious UART activity on the Notecard's diagnostic port that can interfere with diagnosing sleep-path issues.
-
-## 10. Troubleshooting
-
-| Symptom | Likely Cause | Solution |
-|---------|--------------|----------|
-| Notecard doesn't connect to cellular on first power-up. | No ProductUID set, or invalid UID. | Confirm `PRODUCT_UID` in the sketch is your real Notehub project UID (not the placeholder). Flash again and watch the USB serial monitor for error messages. |
-| No `solar_summary.qo` events appear in Notehub. | Device not claiming to project; Notecard is in wrong project. | Open the In-Browser Terminal and send `{"req":"hub.status"}` to check which project the Notecard has claimed. If it's wrong, try `{"req":"hub.set","product":"your-real-product-uid"}` to re-claim. |
-| `solar_summary.qo` events appear, but all strings report `−9999` (no data). | Modbus read failing silently. | Check `modbus_baud`, `modbus_slave_id`, `modbus_parity`, `modbus_stop_bits` match the inverter configuration. Bench-test with a Modbus simulator (see §9 "Modbus first-light"). Look for a `modbus_fail` alert in Notehub. |
-| Alerts fire constantly, even on a healthy array. | `string_stc_w` is set too high, or `perf_thresh_pct` is set too low. | Verify the `string_stc_w` matches your actual string rating. The default `6000` W is for a 15×400 W example; adjust it. Simulate with inflated STC (§9) to test the alert path. |
-| No alerts even when strings are visibly shaded or soiled. | `perf_thresh_pct` is too low, or irradiance is below `irradiance_min_wm2`. | Check if `irradiance_wm2` in the summary is below your threshold (default `100` W/m²). At low light, PR evaluation is intentionally suppressed to avoid noise-driven false positives. Increase `perf_thresh_pct` via Notehub to increase sensitivity (e.g., from 80 to 75). Wait up to 120 minutes for the environment variable to sync. |
-| Idle current is 1 mA or higher (should be ~8–18 µA). | USB/VUSB is connected to Notecarrier; Mojo is not reaching its sleep call. | Disconnect USB from the Notecarrier and power through +VBAT only. Confirm the DIP switch is set to `HST` (not `NC`). Check that `NotePayloadSaveAndSleep` completes on every sample cycle. |
-| `temp_probe_fault` alerts fire. | DS18B20 disconnected, out of range (< −40 °C or > 110 °C), or probe is shorted. | Verify the DS18B20 data wire is connected to D5 with a 4.7 kΩ pull-up to 3V3. Inspect the probe tip for corrosion or water damage. Confirm it's attached to a representative panel backsheet. |
-| Pyranometer reads seem consistently low or high. | Pyranometer is shaded, or unit calibration differs from nominal. | Verify the sensor is in full sun, co-planar with the array, and away from shading objects. Each sensor has a calibration tolerance; the default `pyranometer_mv_per_wm2_x1000 = 200` (0.200 mV/W/m²) is the Apogee SP-110-SS nominal. Update it with your unit's calibration certificate value if available. |
-
-## 11. Limitations and Next Steps
-
-(Note: This is the detailed per-constraint section referenced throughout. For commissioning steps, see §9 Validation and Testing above.)
-
-This reference design targets the moment a portfolio operator wants real per-string visibility on an array — soiling, shading, and bad-module signals delivered to an off-site dashboard within a single sample interval. A handful of details were left simple so the path from BOM to first event stays inside a single afternoon. Each is documented below alongside the production hardening that closes the gap.
-
-### Simplified for this reference design
-
-Each item below is a place where the reference build keeps the signal model or the hardware deliberately simple, with the production hardening that closes the gap.
-
-
-**Per-string voltage is required for full root-cause classification.** The firmware reads a `[V, I]` register pair for each string and uses the per-string voltage signature to classify root causes. This is the signal model provided by multi-MPPT string inverters where each MPPT input tracks one string, such as those implementing the [SunSpec Model 160 Multiple MPPT](https://sunspec.org/sunspec-modbus-specifications/) register model, which exposes per-MPPT voltage (DCV) and current (DCA) via Modbus RTU. Traditional string combiner boxes aggregate multiple strings onto a shared DC bus and typically expose only per-string current at the combiner's Modbus port; **independent per-string voltages are not available in that topology.** Against a combiner-only source the `shading` root-cause hypothesis — which classifies by operating voltage significantly lower than the fleet mean — will never fire, because every string reads the same shared bus voltage and all per-string voltage ratios equal 1.0. In that configuration all PR-threshold trips will resolve as `soiling`, `string_fault`, or `degraded` depending on the current magnitude, and the `shading` classification can never be reached. If the monitored device provides only per-string current and a single bus voltage, document this constraint for O&M personnel so that a `soiling` or `string_fault` alert is understood to also encompass undetected shading.
-
-**Register-map demo only.** The firmware reads N contiguous 16-bit holding-register pairs `[V, I, V, I, …]` with fixed scaling from `reg_base`. Real string combiners and inverters from SMA, Fronius, Huawei, SolarEdge, and ABB all publish their own Modbus register maps with vendor-specific addressing conventions, scaling factors, signed/unsigned handling, and sometimes 32-bit register pairs for accumulated energy, so production deployments need a vendor-specific firmware build that implements the correct map. The environment-variable scaling factors (`string_v_scale_x100`, `string_a_scale_x1000`, `reg_base`) let a single firmware binary cover multiple sites with the same vendor, but **they are not a substitute for correct implementation of the register map.**
-
-**All monitored strings must be electrically comparable.** One device should monitor only peer strings on the same array geometry: equal module count, equal STC rating, and the same orientation, tilt, azimuth, and irradiance/temperature reference (same MPPT group and microclimate). The PR model uses `string_stc_w` as a single reference power for every monitored string, and the V/I root-cause hypothesis (shading/soiling/string_fault) uses the fleet mean of all monitored strings as its reference. On a device monitoring strings across different orientations, different MPPT groups, or unequal-length strings, **normal production differences between groups will be misclassified as faults.** For mixed-geometry arrays, deploy one device per homogeneous string group.
-
-**Maximum 4 strings.** `MAX_STRINGS = 4` is a compile-time constant, while utility-scale installations can have 20–30 strings per combiner. Increasing `MAX_STRINGS` grows `sizeof(AppState)` — the state blob serialized to Notecard flash each sleep cycle — and expands the `note.template` payload definition sent on first boot. The binding constraint is the STM32L433's 64 KB SRAM: `AppState`, the per-Notefile template JSON construction buffers, and the Modbus response buffer must all coexist in static and stack memory. Because the implementation uses no dynamic allocation, the risk is not heap fragmentation but a growing static footprint that leaves too little headroom for library stack frames. Scaling `MAX_STRINGS` beyond 4 requires revalidating that the combined static footprint fits within the 64 KB SRAM budget. No measured ceiling has been established for this implementation, so **validate at each increment before field deployment.**
-
-**RS-485 interface is POC-grade only.** The SparkFun BOB-10124 (SP3485) is a non-isolated, non-surge-hardened breakout board suitable for bench and prototype use. Solar PV combiner and inverter RS-485 ports are exposed to the outdoor environment and share a ground reference with high-voltage DC bus equipment; lightning-induced transients and ground-fault currents are a realistic field hazard. Production hardware should replace the BOB-10124 with an isolated RS-485 transceiver (e.g. Analog Devices ADM2587E or similar) that provides galvanic isolation between the Cygnet UART and the RS-485 bus, and should add appropriate surge protection (TVS diodes rated for the bus, IEC 61000-4-5 Class 4 or better) on the A/B lines. The cable shield must remain single-ended bonded at the combiner/inverter chassis; **proper bonding and grounding practices per IEC 62548 or NEC Article 690 are essential for outdoor PV installations.**
-
-**Analog pyranometer resolution** is adequate for coarse threshold comparisons but limits PR accuracy. The Apogee SP-110-SS directly connected to A0 provides approximately 4 W/m² per ADC count at the Cygnet's 12-bit / 3.3V reference. Adding a single-supply rail-to-rail op-amp in a 10× non-inverting configuration between the sensor and A0 improves effective resolution to about 0.4 W/m² per count and is recommended for production deployments. Alternatively, pyranometers with Modbus RTU output (such as the IMT Si-RS485TC-T-MB, which also includes a Pt1000 module temperature channel) can replace both the SP-110-SS and the DS18B20 with a single RS-485 device on the same bus.
-
-**Single-axis temperature coefficient.** The firmware uses one `temp_coeff` value for the entire array, yet bifacial modules, thin-film modules, and heterojunction (HJT) panels all have meaningfully different temperature coefficients from standard mono-PERC. The default (−0.0035 /°C) is representative for mono-PERC; adjust via `temp_coeff_per10000` for the actual module datasheet value. A per-string coefficient would require a more complex state structure.
-
-**No irradiance-sensor validation.** The pyranometer reading is used as-is with no cross-check, so a bird dropping on the pyranometer lens or a shadow from a nearby object will cause the firmware to compute an artificially low expected power, making the PR appear high and **suppressing real underperformance alerts.** A second reference pyranometer or a model-based irradiance estimate (from a local weather API via a Notehub route) would catch this.
-
-**Heuristic root-cause hypothesis.** The shading/soiling/string-fault classification uses simple thresholds on the ratio of each string's V and I to the fleet mean. It will misclassify strings in certain multi-fault scenarios (e.g., one string with both shading and partial soiling), and the fleet-mean reference breaks down when the majority of strings are simultaneously underperforming. **Treat the hypothesis as a first-pass maintenance triage signal, not a definitive diagnosis.**
-
-**Single-string deployments support PR alerts only.** When `n_strings = 1`, the comparative root-cause hypotheses (`shading`, `soiling`, `string_fault`) are unavailable — there are no peer strings to compare against, so all PR-threshold trips emit as `degraded`. The device still detects when the string's PR drops below `perf_thresh_pct` and fires an immediate alert; the maintenance team must determine the root cause manually from site inspection or additional data. If root-cause classification is required for a single-string installation, a Modbus-output pyranometer with a reference string (even a single extra string of the same geometry) provides the peer comparison needed.
-
-**Mojo is bench-validation equipment only.** The Mojo is spliced into the 5V supply rail and connected to the Notecarrier CX via a Qwiic cable to bench-validate the sleep/wake current profile during commissioning. The Notecard reads Mojo's coulomb counter / power monitor over that Qwiic connection and exposes the data via `card.power`; the application firmware does not call `card.power` on each sample cycle. The Mojo and its Qwiic cable are **not deployed to the field**; remove both before enclosing the assembly. Adding a runtime mAh field to the summary Note by calling `card.power` in firmware and including the result in `solar_summary.qo` is a straightforward extension if fleet-level energy-consumption telemetry is useful.
-
-### Production Next Steps
-
-Once the basic dashboard is live, the following extensions are the natural progression toward a portfolio-grade monitoring product.
-
-**Vendor-specific Modbus register-map implementations** for the major inverter/combiner families (SMA SunnyBoy, Fronius Symo, SolarEdge SetApp, Huawei SUN2000) use SunSpec Model 101 (single-phase/three-phase inverter) and Model 160 (Multiple MPPT) as the standard reference.
-
-**Increase `MAX_STRINGS` to at least 12** and validate that total static memory (AppState + template JSON buffers + library stack frames) fits within the STM32L433's 64 KB SRAM budget.
-
-**A Modbus-output pyranometer+temperature combo sensor** (e.g. IMT Si-RS485TC-T-MB) consolidates three separate interfaces into one RS-485 bus.
-
-**A shadow-mask calendar** suppresses shading alerts between known shadow times (calculable from GPS coordinates and sun-position math) so seasonal early-morning or late-afternoon shadows don't flood the alert channel.
-
-**Field-upgradeable firmware** via [Notecard Outboard Firmware Update](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) lets register-map recipe updates be pushed to the fleet without a site visit.
-
-**Per-device commissioning** records a 7-day baseline PR per string immediately after installation so the alert threshold adapts to site-specific shading and soiling patterns rather than relying on a generic default.
-
-## 12. Summary
-
-For the asset manager who used to find out about a soiled or shaded string only when the quarterly energy report showed a yield gap, the picture is now reversed: every five minutes the device reads what the combiner already knows, contextualizes it against the current irradiance and module temperature, and decides whether each string is performing within bounds. It sends a compact summary every hour and an alert with a root-cause hypothesis (shading, soiling, string fault) within a minute of any string falling below threshold. A Notecarrier CX with a Cell+WiFi Notecard, an RS-485 transceiver, an analog pyranometer, and a DS18B20 backsheet probe is the entire footprint, and none of it touches the existing inverter or asks anything of site IT. The cellular-first deployment model — one SKU, one SIM, no per-site configuration — is what makes that picture economically viable across a portfolio of dozens or hundreds of arrays. It replaces the quarterly look-back with continuous visibility into the 3–8% of annual production that used to leak away silently.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/solar-array-string-level-performance-dashboard/)**
diff --git a/72-solar-array-string-level-performance-dashboard/banner.png b/72-solar-array-string-level-performance-dashboard/banner.png
deleted file mode 100644
index 49ccff56..00000000
Binary files a/72-solar-array-string-level-performance-dashboard/banner.png and /dev/null differ
diff --git a/72-solar-array-string-level-performance-dashboard/diagrams/01-system-architecture.svg b/72-solar-array-string-level-performance-dashboard/diagrams/01-system-architecture.svg
deleted file mode 100644
index bb58bbf9..00000000
--- a/72-solar-array-string-level-performance-dashboard/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/72-solar-array-string-level-performance-dashboard/diagrams/02-wiring-assembly.svg b/72-solar-array-string-level-performance-dashboard/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 0233a8e2..00000000
--- a/72-solar-array-string-level-performance-dashboard/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,95 +0,0 @@
-
diff --git a/72-solar-array-string-level-performance-dashboard/diagrams/03-data-flow.svg b/72-solar-array-string-level-performance-dashboard/diagrams/03-data-flow.svg
deleted file mode 100644
index 073b2c94..00000000
--- a/72-solar-array-string-level-performance-dashboard/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,68 +0,0 @@
-
diff --git a/74-returnable-container-tote-pool-tracker/README.md b/74-returnable-container-tote-pool-tracker/README.md
index 9708ea80..60b33153 100644
--- a/74-returnable-container-tote-pool-tracker/README.md
+++ b/74-returnable-container-tote-pool-tracker/README.md
@@ -1,513 +1,5 @@
-# Returnable Container / Tote Pool Tracker
+# Returnable Container and Tote Pool Tracker
-
+A cellular asset tracking solution for reusable container pools—totes, kegs, and cylinders—that reports location on motion events and daily heartbeats using the Notecard's built-in accelerometer.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Returnable%20Container%20%2F%20Tote%20Pool%20Tracker).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/tote_pool_tracker/](firmware/tote_pool_tracker/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is an [asset location tracking](https://blues.com/solutions-location-tracking/) solution for reusable container pools — plastic totes, pressurized kegs, and gas cylinders — that reports location on motion events and daily heartbeats using cellular connectivity and the [Notecard's](https://shop.blues.com/products/notecard-cellular?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) built-in accelerometer. Containers emit a motion event when they start or stop moving, and a daily confirmation when idle, all over LTE Cat-1 bis. The hardware is a [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) with a Notecard Cell+WiFi (see §4 for the BOM); operators tune heartbeat interval and motion sensitivity from the [Blues Notehub](https://blues.com/notehub/) cloud service without re-flashing.
-
-## 1. Project Overview
-
-**The problem.** Reusable containers are the circulatory system of supply chains: plastic totes move produce from farm to distribution center, pressurized cylinders ferry gases between filling plants and customer sites, stainless kegs make the brewery-to-bar loop thousands of times before retirement. The economics only work when the containers keep circulating. But they leak out of their pools constantly: left on a loading dock past their pickup window, mislaid in a back corner of a customer warehouse, loaded onto the wrong carrier, or simply forgotten at a rail interchange for six months. Industry estimates peg pool shrinkage at anywhere from 5% to 20% annually per container type: a quiet, diffuse cost that rarely generates a single dramatic incident but steadily erodes the pool's working capacity and replacement budget.
-
-The underlying problem is visibility. Traditional approaches all break down somewhere in the supply chain: passive RFID barcodes require scanner infrastructure at every gate, active RFID requires per-site readers the container owner doesn't control, and manual cycle counts are expensive, slow, and never quite synchronized with reality. GPS-plus-cellular seems like the obvious fix, but pure GPS is a poor match for an asset that spends 90% of its time sitting still in a warehouse: the receiver draws tens of milliamps waiting for a fix that adds only marginal precision over simply knowing the container is "at the Memphis DC."
-
-This project takes a better approach. The Notecard's built-in accelerometer watches for motion while the device sits in microamp sleep. When the accelerometer detects the container moving (being picked up by a forklift, loaded onto a truck, or shunted across a rail yard) it wakes the host MCU. The host queues a motion event; the Notecard delivers it over cellular and uses cell-tower and WiFi AP triangulation for location. This adds no meaningful power cost beyond the cellular session already needed to deliver the Note, and it delivers site-level accuracy — sufficient to answer "is this tote at Supplier A or Customer B?" — without GPS hardware or GPS cold-start latency. A daily heartbeat confirms the device is alive even when the container sits undisturbed for a week.
-
-**Why Notecard.** Containers crisscross supplier warehouses, customer sites, truck yards, and rail interchanges, most of which are not on any WiFi network the container owner controls. There is no realistic way to ask a customer to share their wireless network, and no consistent AP infrastructure across a national supplier base. Cellular removes every one of those dependencies and deploys identically at a Chicago cold-storage facility and a rural agri-distribution depot. Location is provided by cell-tower and WiFi AP triangulation (`mode:"wifi,cell"`) — no GPS hardware and no GPS cold-start latency. On each Notehub session the Notecard scans surrounding cell towers and, where WiFi APs are visible, scans those too; Notehub resolves both sources into a latitude/longitude and appends it to every event automatically. In AP-dense environments such as warehouses and distribution centers, WiFi augmentation can improve accuracy from the kilometer scale to tens of meters; in rural or outdoor areas with no APs the device falls back to cell-only triangulation transparently. The WiFi scan adds a modest overhead to each session startup (typically a few seconds in practice, depending on AP density and radio conditions), negligible relative to the cellular session itself, making it an effectively free accuracy improvement in environments where APs are available. This is [asset location tracking](https://blues.com/solutions-location-tracking/) built around the actual geography of the supply chain, not a lab ideal.
-
-
-
-**Bench and limited field-trial assembly.** This reference build uses a Notecarrier CX, a Notecard, and a rechargeable 2 Ah LiPo battery enclosed in a polycarbonate IP67 housing (Hammond 1554C2GY or equivalent. See [§4](#4-hardware-requirements)) and affixed directly to the container using non-penetrating fasteners — stainless zip ties, band clamps, cradle brackets, or industrial-grade adhesive pads depending on the container type. No wiring to the container, no site infrastructure. Battery swaps are expected every 12–24 months in a low-motion deployment, so this hardware stack is suited to bench validation and short-duration field trials rather than a multi-year production rollout. The device self-configures on first power-on and operates autonomously; fleet operators manage all devices and tune per-fleet settings from Notehub. For production deployments requiring 3–5+ years between swaps, a custom carrier board with a Li-SOCl₂ primary cell replaces the LiPo path. See [§11](#11-limitations-and-next-steps).
-
-
-
-**Safety: not rated for classified or explosive atmospheres.** This build uses standard commercial electronics and a LiPo battery. It is **not** ATEX, IECEx, Class I Division 2, or intrinsically safe certified, and must not be deployed in classified or explosive atmospheres, including any location where flammable gas or vapor may be present, unless the complete assembly (enclosure, battery, antenna, and all electronics) has been certified for that environment by a recognized testing body.
-
-
-
-## 2. System Architecture
-
-
-
-### Event Format in Notehub
-
-The firmware emits two Note types; Notehub automatically appends location and routes them independently:
-
-**Motion event (`tote_event.qo`) — appears within ~30 seconds after motion threshold crossed:**
-
-```json
-{
- "file": "tote_event.qo",
- "body": {
- "event": "departed",
- "moving": true,
- "battery_mv": 3851.0,
- "cycle": 14
- },
- "where_lat": 35.9728,
- "where_lon": -86.7653,
- "where_location": "Nashville TN USA",
- "where_country": "US"
-}
-```
-
-**Daily heartbeat (`tote_heartbeat.qo`) — fires once per `heartbeat_hours` (default 24 h):**
-
-```json
-{
- "file": "tote_heartbeat.qo",
- "body": {
- "battery_mv": 3920.0,
- "moving": false,
- "cycle_count": 42,
- "reason": 1
- },
- "sync": true,
- "where_lat": 35.9728,
- "where_lon": -86.7653,
- "where_location": "Nashville TN USA"
-}
-```
-
-The `reason` field is `0` (boot), `1` (heartbeat), or `2` (low battery). The `where_*` fields are appended by Notehub after cell-tower triangulation; no firmware code is required to produce location data.
-
-**Device-side responsibilities.** The Cygnet STM32 host on the Notecarrier CX is, by design, asleep for most of every day — its only job is duty-cycle management and quick decision-making when something does happen. On each wake (triggered by either a motion-state change or the daily heartbeat timer) the Cygnet asks itself why it woke, formats the appropriate Note, hands it off to the Notecard over I²C, and goes back to sleep within seconds. Between wakes the host MCU is powered off entirely by the Notecard's ATTN pin, drawing essentially zero current for the 23+ hours a typical container sits still on a given day. All accelerometer monitoring runs on the Notecard's own low-power IMU; the Cygnet is involved only for the seconds it takes to queue a Note.
-
-**Notecard responsibilities.** The Notecard does the patient work. Its built-in accelerometer watches for motion-state changes continuously and fires the ATTN pin the instant the configured motion threshold is crossed. On every Notehub session it scans surrounding cell towers and nearby WiFi APs for [cell-tower and WiFi triangulation](https://dev.blues.io/blog/cell-tower-wifi-triangulation-without-gps/), which Notehub resolves into a latitude/longitude and appends to every event automatically, with no per-event firmware code required. Notes queue locally through outages and ship in order when connectivity returns, and [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) pushed back from Notehub are delivered on each inbound sync so a pool manager can retune motion sensitivity or heartbeat cadence without a firmware update or a truck roll.
-
-**Notehub responsibilities.** The [Notecard](https://shop.blues.com/products/notecard-cellular?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) manages its own cellular session against the supported carrier networks worldwide via its embedded global SIM and delivers data to [Notehub](https://notehub.io) over the Internet. From there [Notehub](https://dev.blues.io/notehub/notehub-walkthrough/) ingests every event, stores it, resolves the cell-tower data into a human-readable location, and applies project-level routes. The two streams stay deliberately separate at the source (`tote_event.qo` for motion events and `tote_heartbeat.qo` for daily heartbeats) so each can fan out to a different destination at a different urgency without filter logic in the route itself. [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) and [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) are the natural unit of organization for a mixed pool, letting a pool manager dial in different motion thresholds for totes versus kegs versus cylinders without ever forking the firmware.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint. A typical fleet integration routes `tote_event.qo` to a warehouse management system (WMS) or custom fleet portal in real-time, and `tote_heartbeat.qo` to long-term storage for pool health analytics.
-
-## 3. Technical Summary
-
-1. Clone this repo and open `/firmware/tote_pool_tracker/tote_pool_tracker.ino` in Arduino IDE.
-2. Replace the empty `#define PRODUCT_UID ""` with your Notehub project's ProductUID.
-3. Install the **Arduino Core for STM32** and **Blues Wireless Notecard** library (see [§7.1](#71-installing-and-flashing)).
-4. Select board **Blues Cygnet** under **Tools → Board** (the canonical FQBN is `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-5. Click **Upload**. The Notecarrier CX's ST-Link interface appears as a USB device; no external programmer needed.
-6. Power the assembly (USB or battery). On first cellular connect, the device auto-associates with your Notehub project. Within 1–2 minutes it appears in the **Devices** tab.
-7. Tap or shake the assembly firmly for 3–4 seconds. A `tote_event.qo` with `"event":"departed"` should arrive in Notehub within ~30 seconds. Set it down; after 30 seconds of stillness, a `"event":"arrived"` follows.
-
-Detailed assembly, firmware, and Notehub configuration follow in the sections below.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "tote_event.qo",
- "body": {
- "event": "departed",
- "moving": true,
- "battery_mv": 3851.0,
- "cycle": 14
- },
- "sync": true,
- "where_lat": 35.9728,
- "where_lon": -86.7653,
- "where_location": "Nashville TN USA",
- "where_country": "US"
-}
-```
-
-## 4. Hardware Requirements
-
-
-
-**Bench/POC BOM.** The components below equip the bench prototype documented here — the Notecarrier CX and its LiPo charge path are the right platform for bench validation and limited field trials. A production deployment targeting 3–5+ years between swaps requires a custom carrier board with a direct +VBAT input and a Li-SOCl₂ primary cell instead of the LiPo; see [§11](#11-limitations-and-next-steps) for that path.
-
-
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an onboard Cygnet STM32 host MCU — no separate host board needed. ATTN power-gating cuts the Cygnet power rail entirely during sleep. |
-| [Notecard Cell+WiFi (MBGLW)](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) | 1 | Cellular connectivity plus built-in accelerometer for motion detection. The Cell+WiFi variant enables both cell-tower and WiFi AP triangulation (`mode:"wifi,cell"`), improving location accuracy in AP-dense environments while transparently falling back to cell-only where no APs are present. Per the [NOTE-MBGLW datasheet antenna requirements](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/#antenna-requirements), the MBGLW includes a PCB antenna for its WiFi function; **no external WiFi hardware is needed in plastic or polycarbonate enclosures** — the on-board PCB antenna scans nearby WiFi APs through non-metallic walls without modification. The MBGLW also exposes a dedicated WiFi U.FL connector so a 2.4 GHz external antenna can be substituted; other Blues reference builds that deploy the same Notecard inside a metal enclosure (e.g., a commercial HVAC RTU sidecar) do connect an external lead to this connector — that is a consequence of the metal enclosure blocking the on-board PCB antenna's 2.4 GHz signal, not a different hardware topology on the Notecard itself. In this plastic-tote deployment the on-board PCB antenna is the correct and sufficient choice. In metal-enclosure or metal-bodied-container installations, the metal itself shields the 2.4 GHz signal regardless of which antenna is connected internally — both the on-board PCB antenna and any antenna placed entirely inside the metal box are equally blocked; WiFi AP triangulation will be degraded unless the antenna's radiating element is mounted outside the metal shell or positioned behind a verified non-metallic RF window. See the optional WiFi antenna row below. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(commissioning/validation)* | 1 | Coulomb counter spliced inline on the battery rail for ground-truth energy validation during bring-up. Not deployed with the field tracker; kept at the bench as commissioning and regression-testing equipment. |
-| [SparkFun Lithium Ion Battery — 2 Ah (PRT-13855)](https://www.sparkfun.com/products/13855) | 1 | 3.7 V / 2000 mAh LiPo with JST PH connector matching the Notecarrier CX battery port. POC power source; realistic service life is 12–24 months. See [§11](#11-limitations-and-next-steps) for the Li-SOCl₂ production path. |
-| [Hammond Manufacturing 1554C2GY](https://www.hammfg.com/part/1554C2GY) *(field deployment enclosure, IP67/NEMA 4X)* | 1 | 120 × 65 × 40 mm UV-stabilized polycarbonate enclosure, NEMA 4X / IP67–IP68, silicone gasket, flat lid. Internal cavity (~113 × 58 × 34 mm) fits the Notecarrier CX (76 × 38 mm board) oriented lengthwise with the LiPo pack lying flat on top of the board. Rated for outdoor service at truck yards, rail interchanges, port terminals, and keg/cylinder washdown environments. Clear-lid variant **1554C2GYCL** available for installations where visual LED inspection through the lid is useful. Stocked at Mouser, DigiKey, and RS Components. |
-| [Hammond Manufacturing 1591XXBSFLBK](https://www.hammfg.com/part/1591XXBSFLBK) *(bench bring-up only, IP54, not suitable for outdoor or field deployment)* | 1 | 113 × 63 × 28 mm ABS enclosure, IP54, flanged gasketed lid with integral PCB standoffs. Adequate for bench bring-up and sheltered indoor environments only. **Do not use for outdoor field deployment** — truck yards, rail interchanges, port terminals, and container washdown workflows require a minimum IP67 enclosure; use the Hammond 1554C2GY above. Available from Hammond, Mouser, and RS Components. |
-| [SparkFun SMA to U.FL Cable — 150 mm (WRL-18568)](https://www.sparkfun.com/sma-to-u-fl-cable-150mm.html) *(optional, external antenna path)* | 1 | Routes the cellular signal from the Notecard's u.FL socket to an SMA female connector seated in the enclosure wall. Required only for enclosed or stacked-container deployments where the bundled patch antenna has insufficient sky view. |
-| [SparkFun LTE Hinged External Antenna — 698 MHz–2.7 GHz, SMA Male (CEL-16432)](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html) *(optional)* | 1 | Stub antenna (SMA male, 2.3 dBi) covering all LTE Cat-1 bis bands globally; screws directly onto the SMA female end of the pigtail after threading the pigtail's SMA connector through a ¼″ panel hole in the enclosure wall. **Alternate:** [Taoglas TG.30.8113](https://www.taoglas.com/product/apex-tg-30-2g3g4g-terminal-antenna-hinged-ra-sma-m/) (3 dBi, 698–2700 MHz, hinged SMA male) for ~0.7 dB higher gain in challenging coverage environments. |
-| [Blues Flexible Cellular/Wi-Fi Antenna (SKU 233-00006)](https://shop.blues.com/products/flexible-cellular-or-wi-fi-antenna?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(optional, only useful if the radiating element can be placed outside any metal shielding)* | 1 | 85 mm × 14.5 mm flexible FPC antenna (Molex), 698 MHz–4.0 GHz, 180 mm u.FL coaxial tail. Connect to the Notecard's **WiFi U.FL connector**. For metal-enclosure or metal-bodied installations: this antenna is only effective if the radiating element is routed **outside** the metal — through a cable gland or behind a dedicated non-metallic RF window in the enclosure wall. An antenna placed entirely inside a metal enclosure is still shielded by the metal and provides no improvement over the on-board PCB antenna. **Not required for plastic or polycarbonate enclosures**, where the MBGLW's on-board PCB WiFi antenna scans nearby APs through the non-metallic walls without modification. |
-| [Essentra Components CG-M16-1-BK Cable Gland, M16, IP68](https://www.essentracomponents.com/en-us/p/cable-glands-straight/cg-m16-1-bk) *(optional, cable-through variant only)* | 1 | Only required if substituting a remote-mount antenna with an integrated SMA cable for the stub antenna above (e.g., a magnetic-mount antenna on the exterior of the shipping container). Seals the cable exit through the enclosure wall; accepts 3–10 mm cable diameters. |
-
-Blues hardware ships with an active SIM including 500 MB of data and 10 years of service: no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-One of the quiet advantages of this design is how little wiring there is to talk about. The Notecard's built-in accelerometer handles all motion detection on its own, and on plastic or polycarbonate enclosures the MBGLW's on-board PCB WiFi antenna takes care of AP scanning without any external hardware. That leaves five short steps between an empty enclosure and a tracker ready to clip to a container: seat the Notecard, connect the antennas, connect the battery, (optionally) splice in the Mojo for bench validation, and close the enclosure.
-
-**1. Notecard.** Slide the Notecard Cell+WiFi (MBGLW) into the Notecarrier CX's M.2 slot, label-side up, and press until it seats. The Notecarrier CX's Cygnet host communicates with the Notecard over the I²C bus routed through the M.2 interface; no jumper wires or external connectors needed.
-
-**2. Antennas.** The MBGLW has two antenna connections.
-
-*Cellular:* Connect the bundled cellular patch antenna to the Notecard's cellular **u.FL** connector. For sealed polycarbonate enclosures or stacked-container deployments where the patch antenna has limited sky view, route a u.FL-to-SMA pigtail to an external stub antenna through a cable gland in the enclosure wall (see BOM optional items). The bundled patch antenna is sufficient for bench testing and most open-top plastic housings.
-
-*WiFi:* Per the [NOTE-MBGLW datasheet antenna requirements](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/#antenna-requirements), the MBGLW includes a PCB antenna for its WiFi function — **no connection or external hardware is needed** for plastic or polycarbonate enclosures. The on-board PCB antenna scans nearby APs through the non-metallic walls at each Notehub session, and Notehub resolves that data to a location automatically. The MBGLW also exposes a WiFi U.FL connector; an external 2.4 GHz antenna can be substituted there when the enclosure material demands it. Reference builds that deploy the same Notecard inside a fully metal enclosure do use an external lead connected to this WiFi U.FL connector — that is because the metal enclosure blocks the on-board PCB antenna's 2.4 GHz signal equally; the connector exists on every MBGLW unit regardless. In this plastic-tote deployment the on-board PCB antenna is the correct choice and no WiFi U.FL connection is required. If the device will be housed in a metal enclosure or mounted within a metal-bodied container, Note that metal shields the 2.4 GHz signal: **both the on-board PCB antenna and any antenna left entirely inside the metal are equally blocked**, so simply connecting a flex antenna to the WiFi U.FL connector without routing it outside the metal will not help. To retain WiFi AP scanning in a shielded installation, the antenna's radiating element must be positioned outside the metal shell — route a 2.4 GHz u.FL antenna (e.g., Blues Flexible Cellular/Wi-Fi Antenna, SKU 233-00006) through a cable gland or position it behind a non-metallic RF window in the enclosure wall, then connect the u.FL tail to the Notecard's **WiFi U.FL connector**. Where WiFi scanning is not essential, the device will fall back to cell-only triangulation transparently.
-
-**3. Battery.** Connect the SparkFun 2 Ah LiPo to the Notecarrier CX's 2-pin JST PH battery connector (labeled `LIPO`). Polarity is enforced by the keyed connector; do not force a reversed pack. The Notecarrier CX includes a LiPo charge circuit: the battery charges when USB power is present and the tracker runs from battery when USB is removed.
-
-**4. Mojo (bench bring-up only).** For power validation during development, splice the Mojo inline between the LiPo JST connector and the Notecarrier CX `LIPO` input: Mojo's `BAT+` / `BAT-` connects to the battery, and Mojo's `LOAD+` / `LOAD-` connects to the Notecarrier CX. The Mojo Qwiic connector attaches to the Notecarrier CX's Qwiic port; it reports cumulative mAh to the Notecard over I²C. This measurement covers the full assembly — Cygnet plus Notecard — under battery conditions. **Note:** this firmware does not read Mojo data over I²C or publish any Mojo telemetry in Notes; Mojo is used exclusively as a bench commissioning and regression-testing instrument and is not deployed with the field tracker.
-
-**5. Enclosure.** Mount the board and battery in the polycarbonate enclosure (Hammond 1554C2GY for field deployment; 1591XXBSFLBK for bench-only) with any external cellular antenna cable routed through a cable gland. Affix the housing to the container using non-penetrating fasteners only: stainless zip ties or hose-clamp style straps on plastic totes, band clamps or collar brackets on kegs and cylinders, industrial-grade VHB adhesive pads on flat polymer surfaces.
-
-
-
-**Safety: pressurized vessels must not be drilled or tapped.** Kegs, gas cylinders, and any other pressurized container must never be drilled, tapped, welded, or structurally modified in any way to mount a tracker. Use only external band clamps, collar brackets, or strap-style mounts that attach to the vessel's exterior surface without breaching its structural integrity.
-
-
-
-
-
-**Safety: not rated for classified or explosive atmospheres.** This assembly is not ATEX, IECEx, Class I Division 2, or intrinsically safe certified. Do not deploy it in any classified or explosive atmosphere, including flammable-gas environments associated with certain gas-cylinder applications — without a fully certified enclosure, battery, antenna, and complete assembly.
-
-
-
-
-
-**Important: ATTN pin power-gating.** The Notecarrier CX routes the Notecard's ATTN pin to the Cygnet's power-enable input, which is what allows `NotePayloadSaveAndSleep` to cut the host rail entirely between cycles — dropping the Cygnet to essentially zero current draw. If you adapt this firmware to a different carrier board, verify that the ATTN-to-host-power wiring is equivalent. Without it, the host never actually powers off and battery life suffers dramatically. See the [Attention Pin Guide](https://dev.blues.io/guides-and-tutorials/notecard-guides/attention-pin-guide/) for wiring details.
-
-
-
-## 6. Notehub Setup
-
-**1. Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) — it looks like `com.your-company.your-name:tote-tracker`.
-
-**2. Set the ProductUID in firmware.** Open [`tote_pool_tracker.ino`](firmware/tote_pool_tracker/tote_pool_tracker.ino) and replace the empty string on the `#define PRODUCT_UID ""` line (near the top) with your value. Alternatively, pass it as a build flag: `-DPRODUCT_UID=\"com.your-company.your-name:tote-tracker\"`.
-
-**3. Claim the device.** Power the assembly. On first cellular connect the Notecard associates with your Notehub project automatically; no manual claim step required. The device appears in the **Devices** tab within a minute or two.
-
-**4. Organize into Fleets.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. A natural grouping for a mixed container pool is one fleet per container type (totes, kegs, cylinders) or per customer region — each fleet can carry different `motion_threshold` values tuned to the handling environment. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can auto-assign new devices to the correct fleet based on device name or serial number prefix, avoiding manual configuration at scale.
-
-**5. Set environment variables.** In Notehub, select the fleet you created above and navigate to **Environment** (left sidebar). These variables are optional; firmware defaults are shown. The device fetches them on its next inbound sync; changes take effect without re-flashing.
-
-| Variable | Default | Purpose |
-|---|---|---|
-| `heartbeat_hours` | `24` | Hours between heartbeat Notes when the container is stationary. Range: 1–168. Longer intervals extend battery life; shorter intervals keep "last-seen" timestamps fresh. Heartbeat Notes are sent with `sync:true`, so each fires its own cellular session immediately — delivery is not dependent on the `outbound` periodic timer. Changing this value reapplies `hub.set outbound/inbound` in the same wake cycle to keep the Notecard's inbound polling cadence (environment variable pulls) aligned with the heartbeat schedule. |
-| `low_battery_mv` | `3300` | Battery rail voltage (mV) below which the heartbeat Note's `reason` code becomes `2` (low battery). Use as a signal to schedule a battery swap before the tracker goes dark. |
-| `motion_threshold` | `5` | Accelerometer events per time bucket required before the Notecard calls the container "moving." Raise if totes trigger false departures from vibrating dock floors; lower for gentle-handling environments like roller conveyors. Range: 1–20. |
-| `motion_bucket_sec` | `30` | Duration of each accelerometer counting bucket (seconds). Paired with `motion_threshold` to tune sensitivity vs. response latency. Range: 5–300. |
-
-**6. Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `tote_event.qo` (immediate motion events, WMS, fleet portal, or alert webhook) and a second for `tote_heartbeat.qo` (daily pool health, long-term analytics store). Separate Notefiles at the source let each route target a different destination and urgency level without any filtering logic in the route.
-
-### What to expect in Notehub
-
-The Notecard initiates an early session on first boot, so events typically appear within a few minutes. Exact timing depends on network registration and initial sync; a weak-signal site or busy carrier can push first appearance to 5–10 minutes. Once registered, the **Events** tab should show:
-
-- **`_session.qo`** — automatic Notecard housekeeping on each cellular session. Confirms the radio is reaching Notehub.
-- **`tote_heartbeat.qo`** — one per `heartbeat_hours` (default 24 h), sent with `sync:true` so each arrives in Notehub within a session-establishment window of the on-device timer firing. The `reason` field is `0` on first power-on (boot), `1` on subsequent timer wakes (heartbeat), and `2` when the battery is below `low_battery_mv`. Notehub appends `where_lat`, `where_lon`, and `where_location` once cell-tower data has been resolved.
-- **`tote_event.qo`** — emitted with `sync:true` when the configured motion threshold is crossed (container starts or stops moving). The `event` field is `"departed"` (stopped → moving) or `"arrived"` (moving → stopped).
-
-## 7. Firmware Design
-
-The firmware spans three files in the `firmware/` directory; keep all three together, since the helpers are referenced from the main sketch:
-
-| File | Role |
-|------|------|
-| [`tote_pool_tracker.ino`](firmware/tote_pool_tracker/tote_pool_tracker.ino) | Main sketch — `setup()`, `loop()`, global definitions |
-| [`tote_pool_tracker_helpers.h`](firmware/tote_pool_tracker/tote_pool_tracker_helpers.h) | Shared types, constants, and `extern` declarations |
-| [`tote_pool_tracker_helpers.cpp`](firmware/tote_pool_tracker/tote_pool_tracker_helpers.cpp) | All helper-function implementations |
-
-The Arduino toolchain automatically compiles the `.h` and `.cpp` alongside the `.ino` when you open or build the sketch directory.
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Add the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**. Select **Blues Cygnet** as the board target.
-- **`Blues Wireless Notecard`** library — [`note-arduino`](https://github.com/blues/note-arduino). Install via the Arduino Library Manager (search "Blues Wireless Notecard") or `arduino-cli lib install "Blues Wireless Notecard"`. See the [note-arduino releases page](https://github.com/blues/note-arduino/releases) for the version available when you install.
-
-**Flashing — Arduino IDE:** Open `firmware/tote_pool_tracker/tote_pool_tracker.ino`, select the Cygnet board under Tools → Board, and click Upload. The Notecarrier CX presents the ST-Link interface over USB; no external programmer needed.
-
-**Flashing — `arduino-cli`:**
-
-```bash
-# Identify the correct FQBN for your installed core version
-arduino-cli board listall | grep -i cygnet
-
-# Compile and upload — point at the sketch *directory* so arduino-cli
-# picks up tote_pool_tracker_helpers.cpp automatically.
-# Replace the FQBN and port with what listall reports.
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/tote_pool_tracker
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/tote_pool_tracker
-```
-
-Replace `/dev/cu.usbmodem*` with the actual port — `COMx` on Windows, `/dev/ttyACM*` on Linux.
-
-After upload, serial output is **disabled by default** — release builds intentionally omit Serial calls to avoid the ~1 mA UART wake-time penalty on a battery-powered device. To enable logging, uncomment `#define DEBUG` in `firmware/tote_pool_tracker/tote_pool_tracker_helpers.h` and reflash; then open the serial monitor at **115200 baud** to watch each wake cycle. The host prints a few lines of output on first boot and on each subsequent wake, then goes quiet during sleep — a few seconds of activity per cycle is normal.
-
-### 7.2 Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard one-time configuration (`hub.set`, `card.voltage`, `card.triangulate`, `card.motion.mode`) | `notecardConfigure()` |
-| Heartbeat Notefile template registration | `defineTemplates()` |
-| Environment variable fetch and clamped override | `fetchEnvOverrides()` |
-| Motion configuration reapplication when env vars change | inline in `setup()` after `fetchEnvOverrides()` |
-| `hub.set` outbound/inbound reapplication when `heartbeat_hours` changes | inline in `setup()` after `fetchEnvOverrides()` |
-| Motion state read from Notecard accelerometer | `readMotionMoving()` |
-| Battery voltage read from Notecard rail monitor | `readBatteryMv()` |
-| Immediate motion event emission (`sync:true`) | `sendMotionEvent()` |
-| Immediate daily heartbeat emission (`sync:true`, template-backed) | `sendHeartbeat()` |
-| State serialization, sleep arming, and host power-off | `enterSleep()` |
-
-### 7.3 Motion and location strategy
-
-Motion detection runs entirely on the Notecard's built-in accelerometer, configured via [`card.motion.mode`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-motion-mode). The `motion` parameter sets a threshold: how many accelerometer events must occur within a configurable time bucket before the Notecard transitions the motion status from `"stopped"` to `"moving"` (or vice versa). When the status changes, the Notecard fires the ATTN pin, which wakes the Cygnet host. Reading the current status is a simple `card.motion` request; the Notecard returns `"mode":"moving"` or `"mode":"stopped"`.
-
-Location is provided by [`card.triangulate`](https://dev.blues.io/notecard/notecard-walkthrough/time-and-location-requests/#using-cell-tower-and-wifi-triangulation) configured with `mode:"wifi,cell"` — both cell-tower scanning and WiFi AP scanning are enabled. On each Notehub session the Notecard scans surrounding cell towers and nearby WiFi APs; Notehub resolves the combined data to a latitude/longitude and appends the resolved position (`where_lat`, `where_lon`, `where_location`, `where_country`) to every event automatically, with no GPS hardware and no per-event firmware code required. In AP-dense environments such as warehouses and distribution centers, WiFi augmentation improves accuracy from the kilometer scale to tens of meters. Where no APs are detectable the Notecard falls back to cell-only triangulation transparently. The WiFi scan adds a modest overhead to session startup (typically a few seconds in practice, depending on AP density and radio conditions), negligible relative to the LTE Cat-1 bis cellular session, making it an effectively free accuracy improvement in AP-rich environments. For this use case (determining which site or city a container is at) cell-plus-WiFi triangulation delivers the right precision at far lower power than GPS. WiFi AP scanning uses the MBGLW's on-board PCB WiFi antenna and works through plastic and polycarbonate enclosure walls without any external hardware; in metal-enclosure or metal-shielded installations the 2.4 GHz signal is blocked and WiFi AP triangulation will be degraded unless the antenna's radiating element is positioned outside the metal shell (see [§5](#5-wiring-and-assembly)).
-
-### 7.4 Event payload design
-
-**`tote_event.qo`** (motion events, untemplated, `sync:true`):
-
-```json
-{
- "file": "tote_event.qo",
- "body": {
- "event": "arrived",
- "moving": false,
- "battery_mv": 3851.0,
- "cycle": 14
- },
- "sync": true
-}
-```
-
-Motion events are untemplated; they're low-volume (at most a handful per day per container) and the flexible JSON schema is useful while the design evolves. `sync:true` tells the Notecard to bypass the `outbound` cadence timer and initiate a cellular session as soon as the Note is queued, so the event typically reaches Notehub within a session-establishment window of 15–60 seconds after the motion threshold is crossed (depending on network conditions and the configured bucket duration). Notehub appends triangulated `where_*` fields before forwarding to any configured routes.
-
-**`tote_heartbeat.qo`** (daily, `sync:true`, [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates)):
-
-```json
-{
- "file": "tote_heartbeat.qo",
- "sync": true,
- "body": {
- "battery_mv": 3920.0,
- "moving": false,
- "cycle_count": 42,
- "reason": 1
- }
-}
-```
-
-`sync:true` causes the Notecard to open a cellular session as soon as the Note is queued, guaranteeing that each daily heartbeat arrives in Notehub within a session-establishment window of the on-device timer firing, not deferred to the next periodic outbound sync, and not dependent on whether a motion-triggered session happens to occur first. Heartbeats use a Note template registered at cold boot. All four fields are fixed-width: `battery_mv` (4-byte float), `moving` (1-byte bool), `cycle_count` (4-byte uint), `reason` (1-byte int), so every record encodes as a compact fixed-length binary entry on the Notecard rather than a variable-length JSON blob. Over a three-year deployment at one heartbeat per day, the bandwidth saving relative to raw JSON is material on a constrained SIM data plan. The `reason` field is a numeric code: `0` = boot (first power-on), `1` = heartbeat (normal timer wake), `2` = heartbeat_low_battery (battery below `low_battery_mv`). Using a numeric type keeps the record fully fixed-width.
-
-### 7.5 Low-power strategy
-
-The firmware's entire application logic runs in `setup()`. After queuing the appropriate Note, `enterSleep()` serializes the device state into the Notecard and calls `NotePayloadSaveAndSleep()`, which issues a [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) request with `mode:"sleep,arm,motionchange"` and a `seconds` value equal to the remaining time until the absolute `next_heartbeat_epoch` deadline, so motion wakes re-arm only the time still owed rather than resetting a fresh full interval. When the epoch is unavailable (before the first cellular sync), the function falls back to the full configured interval. When `failed_send_count` is non-zero, sleep is additionally capped at `RETRY_WAKE_SEC` (15 minutes) so a pending Note is retried promptly. The Notecard pulls the ATTN pin low; the Notecarrier CX's ATTN-to-power-enable circuit then cuts power to the Cygnet entirely. The host MCU cold-boots from `setup()` on every wake — `loop()` is never reached.
-
-The `"arm,motionchange"` addition to the sleep mode means the Notecard wakes the host on whichever fires first: a motion-state transition (accelerometer threshold crossed) or the heartbeat timer expiry. The `arm` keyword is required for `motionchange` to actually fire ATTN during the sleep window — without it, the keyword is accepted in the mode string but the wake source is never armed (per the [asset tracking guide](https://dev.blues.io/guides-and-tutorials/notecard-guides/asset-tracking-with-gps/#wake-host-or-send-note-on-motion-status-change)). Sleep current remains very low (the Cygnet is completely off), but total battery energy is driven mainly by the number of cellular sessions — each daily heartbeat and each `"departed"`/`"arrived"` motion event initiates its own cellular session via `sync:true`. A container that moves several times per day therefore burns materially more energy than one that sits still for a week.
-
-Both Note types are queued with `sync:true`, which causes the Notecard to open a cellular session as soon as the Note is ready, bypassing the `outbound` cadence timer entirely. This guarantees that each daily heartbeat arrives in Notehub within a session-establishment window of the on-device timer firing, not deferred to the next periodic outbound sync, and unaffected by any intervening motion-triggered sessions. [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound`/`inbound` is still set to match `heartbeat_hours` and is reapplied at runtime whenever the value changes, keeping the Notecard's inbound polling cadence — which pulls environment variable updates from Notehub — aligned with the heartbeat schedule.
-
-### 7.6 Retry and error handling
-
-- The first Notecard request (`hub.set` inside `notecardConfigure()`) uses `sendRequestWithRetry(req, 10)` to handle the known cold-boot I²C race condition where the Cygnet comes up before the Notecard is ready to accept transactions.
-- `readMotionMoving()` returns the previously-saved `was_moving` state if `card.motion` returns NULL, preventing a transient I²C failure from being misinterpreted as a genuine motion-state change and triggering a spurious event.
-- `readBatteryMv()` returns `0.0` on NULL response. The `battery_mv: 0` sentinel value in a Note is distinguishable from a normal in-range reading and serves as a flag that the voltage was unavailable, rather than silently substituting a misleading non-zero value.
-- `fetchEnvOverrides()` commits resolved environment values to both the runtime `g_*` globals and the `g_state.desired_*` fields persisted across sleep. On failure (NULL response or Notecard error) neither is modified, so a transient connectivity outage cannot silently revert fleet tuning to compile-time defaults.
-- After `fetchEnvOverrides()` returns, `setup()` compares the desired `g_motion_threshold` / `g_motion_bucket_sec` values against `g_state.last_applied_motion_threshold` / `g_state.last_applied_motion_bucket_sec`. When they differ, `card.motion.mode` is reissued and the `last_applied_*` fields are updated only on a confirmed success, so a failed reissue is retried automatically on the next wake.
-- `heartbeat_hours` is handled identically via `g_state.last_applied_heartbeat_hours`: a desired value that differs from the last applied triggers a `hub.set` reissue with the updated `outbound`/`inbound` cadence in the same wake cycle. The absolute heartbeat deadline is also reanchored immediately to the new interval (when a valid epoch is available), so the device starts sleeping toward the updated schedule without first exhausting the old one.
-- On a retry wake where the pending Note finally succeeds, Branch 2 compares the current motion state against `g_state.pending_moving` (the motion state captured when the pending record was originally created) and emits a follow-up event if they differ. For example: if a `"departed"` Note fails and the container stops before the retry succeeds, `pending_moving` is `true` (container was moving at the time the event was queued), `now_moving` is `false`, and an `"arrived"` event is emitted on the same retry wake. `pending_moving` is used as the baseline rather than `g_state.was_moving` because `was_moving` is updated at the end of every wake including failed retry wakes, so it converges toward the current motion state across multiple retries and would suppress the comparison; `pending_moving` is written exactly once per pending record and never modified during retries. **Design limitation:** the firmware holds at most one Note pending at a time. If the container transitions more than once during an extended retry period, only the final motion state is compared against the original baseline; intermediate transitions that reverse and then reverse again are not recoverable with this single-pending-Note model.
-
-### 7.7 Key code snippet 1: combined motion + timer sleep
-
-After queuing the wake-cycle Note, the firmware serializes state and arms a dual-condition sleep. Sleep duration is computed from the remaining time to the absolute heartbeat deadline so motion wakes do not reset the daily schedule; it is further capped at `RETRY_WAKE_SEC` when a Note needs retrying.
-
-```cpp
-void enterSleep(uint32_t now_epoch) {
- NotePayloadDesc payload = {0, 0, 0};
- NotePayloadAddSegment(&payload, STATE_SEG_ID, &g_state, sizeof(g_state));
-
- // Sleep for the remaining time to the absolute deadline so that motion
- // wakes do not reset the daily schedule. Falls back to the full interval
- // when the epoch is unavailable (before first cellular sync).
- uint32_t sleep_sec = g_heartbeat_hours * 3600UL;
- if (now_epoch > 0 && g_state.next_heartbeat_epoch > now_epoch) {
- sleep_sec = g_state.next_heartbeat_epoch - now_epoch;
- }
-
- // Cap at RETRY_WAKE_SEC when a note.add failed this wake so the pending
- // note is retried promptly rather than waiting a full heartbeat interval.
- if (g_state.failed_send_count > 0 && RETRY_WAKE_SEC < sleep_sec) {
- sleep_sec = RETRY_WAKE_SEC;
- }
-
- // "arm,motionchange" adds a second wake source: ATTN also fires if the
- // accelerometer-based motion status changes before the timer expires.
- // "arm" is required so motionchange actually arms — without it the
- // keyword is silently accepted but the wake source never fires.
- NotePayloadSaveAndSleep(&payload, sleep_sec, "arm,motionchange");
-
- // Execution normally never reaches here — the Notecard cuts Cygnet power.
- delay(30000);
-}
-```
-
-### 7.8 Key code snippet 2: cell-tower and WiFi AP triangulation configuration
-
-Configured once at cold boot. On every subsequent Notehub session the Notecard scans cell towers and nearby WiFi APs automatically; Notehub resolves the combined data and appends the position to each event.
-
-```cpp
-J *req = notecard.newRequest("card.triangulate");
-JAddStringToObject(req, "mode", "wifi,cell");
-JAddBoolToObject(req, "on", true);
-JAddBoolToObject(req, "set", true);
-notecard.sendRequest(req);
-```
-
-### 7.9 Key code snippet 3: motion event with immediate sync
-
-`sync:true` bypasses the outbound cadence timer and initiates a cellular session as soon as the Note is queued. The event typically reaches Notehub within a session-establishment window of 15–60 seconds after the motion threshold is crossed.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", FILE_EVENTS);
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "event", event_type); // "departed" or "arrived"
-JAddBoolToObject(body, "moving", moving);
-JAddNumberToObject(body, "battery_mv", battery_mv);
-JAddNumberToObject(body, "cycle", (int)g_state.cycle_count);
-notecard.sendRequest(req);
-```
-
-### 7.10 Key code snippet 4: heartbeat Note template
-
-All four fields are fixed-width, so every heartbeat record encodes as a compact fixed-length binary entry. `TFLOAT32`, `TBOOL`, and `TUINT32` are type-hint macros from note-c; `11` is the Note template system's 1-byte integer placeholder (as used throughout the reference accelerators).
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", FILE_HEARTBEAT);
-JAddNumberToObject(req, "port", 10);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "battery_mv", TFLOAT32); // 4 bytes — IEEE-754 float
-JAddBoolToObject(body, "moving", TBOOL); // 1 byte — boolean
-JAddNumberToObject(body, "cycle_count", TUINT32); // 4 bytes — unsigned 32-bit int
-JAddNumberToObject(body, "reason", 11); // 1 byte — 0=boot,1=heartbeat,2=low_battery
-notecard.sendRequest(req);
-```
-
-## 8. Data Flow
-
-
-
-**Collected.** On each wake cycle: current motion state (`"moving"` or `"stopped"` from the Notecard's internal accelerometer), battery voltage from the Notecard's rail monitor, and the wake-cycle counter. No external sensors are read; the Cygnet is active for only a few seconds per cycle.
-
-**Transmitted.**
-
-- `tote_event.qo` — queued with `sync:true` on every motion-state transition; the Notecard initiates a cellular session as soon as the Note is ready, bypassing the outbound cadence timer. A normally-active container generates a `"departed"` event when it starts moving and an `"arrived"` event when it stops, producing a matched pair per transit leg. At each session, the Notecard delivers the Note to Notehub, which appends triangulated location before passing it to any configured routes.
-- `tote_heartbeat.qo` — emitted once per `heartbeat_hours` (default 24 h) regardless of motion activity, queued with `sync:true` and delivered immediately via its own cellular session. Guarantees a daily "last seen alive" confirmation in Notehub even for containers idle for days or weeks, regardless of any intervening motion-triggered sessions.
-
-**Routed.** Both Notefiles pass through Notehub, which resolves and appends `where_lat`, `where_lon`, `where_location`, `where_country`, and `where_timezone` to each event as it arrives. From Notehub, [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) can fan the events out to any supported destination — HTTP/HTTPS webhook to a WMS (warehouse management system) or TMS (transportation management system), MQTT to a real-time fleet portal, or Snowflake/cloud storage for historical pool analytics. Route configuration is project-specific; this reference design ships no downstream endpoint.
-
-**In-band alerts.** The `reason` code `2` (low battery) in `tote_heartbeat.qo` is the primary in-band signal for battery maintenance scheduling. Additional alerting logic, such as detecting containers outside expected site boundaries, is best implemented downstream, not in firmware; see [§11](#11-limitations-and-next-steps).
-
-## 9. Validation and Testing
-
-**Expected steady-state.** A healthy tracker on an idle container generates one `tote_heartbeat.qo` per day and zero `tote_event.qo` events; both Note types use `sync:true`, so each fires its own cellular session and arrives in Notehub within a session-establishment window of the on-device event. A tracker on an active container generates between 0 and a dozen `tote_event.qo` events per day (one `"departed"` and one `"arrived"` per transit leg) plus one `tote_heartbeat.qo`. If the Events tab in Notehub shows neither file after 5 minutes, the most likely causes are a missing or incorrect `PRODUCT_UID`, a disconnected antenna, or the device running from USB only (some bring-up environments supply USB power but not battery, which can affect the ATTN power-gating path on the Cygnet).
-
-**Triggering a motion event on the bench.** With the assembly powered and the serial monitor open at 115200 baud, tap or shake the Notecarrier CX firmly a few times. The Notecard's internal accelerometer runs at a low sample rate during motion-detect mode, so it takes a few seconds of sustained motion to accumulate enough events to cross the bucket threshold; a firm 3–4 second tap sequence is typically enough to cross the default threshold of 5. You should see a `tote_event.qo` with `"event":"departed"` appear in Notehub within ~30 seconds. Set the device down and wait the bucket duration (default 30 seconds of inactivity) and you'll see `"event":"arrived"` follow.
-
-**Using Mojo to validate power behavior.** The Notecard's published idle current in low-power mode is approximately 8 µA at 3.7 V between Notehub sessions; see the [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for authoritative measured figures across SKUs and operating conditions. Transmit bursts for a single queued Note are typically seconds to tens of seconds at 50–250 mA average on LTE Cat-1 bis, dominated by radio warm-up and network registration. The Cygnet, when cut by the ATTN pin, draws essentially zero from the rail.
-
-Expected 24-hour energy at one heartbeat per day with no motion events and good signal (bench prototype, LiPo supply):
-
-| Phase | Duration | Approx. current | Approx. energy |
-|---|---|---|---|
-| Deep sleep (Notecard idle, Cygnet off) | ~23.98 h | ~8 µA | ~0.19 mAh |
-| Heartbeat wake and Note queue (~5 seconds Cygnet + I²C) | ~5 seconds | ~15–20 mA | ~0.03 mAh |
-| Heartbeat sync:true cellular session (~20 seconds at ~150 mA avg LTE Cat-1 bis) | ~20 seconds | ~150 mA | ~0.83 mAh |
-| **Total active drain** | | | **~1.1 mAh/day** |
-
-On paper, the 2000 mAh LiPo's active-drain budget alone would last ~5 years. **However, this ignores LiPo self-discharge, which dominates at this duty cycle.** A typical 2000 mAh LiPo self-discharges at 1–2% per month (~480 mAh/year), consuming roughly half the pack capacity over 2 years even while sitting still. Accounting for self-discharge and cell aging, a **realistic LiPo service life is 12–24 months in a low-motion, good-signal deployment**. Weak-signal retries, motion events, and low-temperature operation each further reduce that window. This is why this build is documented as bench/POC rather than production. Multi-year deployments require the Li-SOCl₂ primary-cell path described in [§11](#11-limitations-and-next-steps).
-
-Multi-year service life — 3 to 5+ years between swaps — requires a Li-SOCl₂ primary cell (under 1% annual self-discharge, much higher volumetric capacity). That path requires a different carrier with no charging circuitry; see [§11](#11-limitations-and-next-steps). The energy figures in the table above are still useful for sizing the active-drain budget regardless of chemistry.
-
-Useful Mojo bench validation: leave the assembly running for 24 h and confirm the Mojo tally is in the 1–5 mAh range. Deviations larger than ~5× typically indicate one of:
-
-- **Host never sleeping:** flat 10–80 mA continuous baseline. Most common cause: ATTN power-gating is not active, or `NotePayloadSaveAndSleep` is returning early. On the Notecarrier CX the Notecard's ATTN pin is routed internally to the `EN` input (described as "Input that gates the board's host 3.3V rail" in the [Notecarrier CX header table](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/)) — this is a fixed PCB trace, not a user jumper. If you are not seeing host power-gating, confirm you are using a Notecarrier CX specifically (not a different carrier board), and that no external signal is holding `EN` high.
-- **Excessive cellular retries:** sync bursts are longer than expected or firing more than once per day. Check signal strength via [`card.wireless`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-wireless) or [`hub.status`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-status) in the blues.dev In-Browser Terminal.
-- **Motion false triggers:** `tote_event.qo` Notes are accumulating from dock-floor vibration or transport. Raise `motion_threshold` via the Fleet environment variable and watch the event rate drop without reflashing.
-
-## 10. Troubleshooting
-
-**Device doesn't appear in Notehub Devices tab after 3–5 minutes.**
-
-- Verify the `PRODUCT_UID` in your sketch matches exactly (copy-paste from Notehub, not retyped). A mismatch is the most common cause.
-- Check the serial monitor at 115200 baud (uncomment `#define DEBUG` in `tote_pool_tracker_helpers.h` and reflash to see Notecard I²C trace output).
-- Confirm USB power is connected or the LiPo battery is properly seated in the JST connector (polarity enforced by keyed connector; do not force).
-- Verify the Notecard is fully seated in the M.2 slot.
-- Check signal strength: the Notecard may be registered but slow to sync. Wait up to 10 minutes in a weak-signal area.
-
-**Device appears but never sends an event.**
-
-- Shake or tap the assembly firmly for 3–4 seconds. The accelerometer's low sample rate during motion-detect mode requires sustained motion to accumulate events. Gentle handling or a single brief tap is not enough to cross the motion threshold.
-- Confirm the motion threshold is tuned for your environment. If running high (e.g., `motion_threshold: 15`), normal dock handling may not trigger detection. Lower the value via the **Fleet → Environment** panel and wait for the next inbound sync (~24 hours by default, or manually on the next cellular session if you bump a motion event).
-- Verify that `tote_heartbeat.qo` appears at least once per day in the **Events** tab. If heartbeats never arrive, motion events won't either (same root cause, connectivity or configuration).
-
-**Event arrives once per minute instead of at motion state changes.**
-
-- The motion threshold is too sensitive. Raising `motion_threshold` from 5 to 10–15 in **Fleet → Environment** reduces false triggers from dock vibration. Increase `motion_bucket_sec` to 60 or higher to require sustained motion over a longer window.
-
-**Battery drains much faster than expected (>5 mAh/day consumed).**
-
-- Confirm the ATTN pin power-gating is active: use the Mojo coulomb counter or measure the Cygnet current rail directly. If the host is drawing continuous 10–80 mA, the ATTN power-gating is not functioning. On a Notecarrier CX the ATTN pin is internally routed to the `EN` input (confirmed in the board's [v1.3 datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/)). If adapting this design to a different carrier, verify the ATTN-to-host-power wiring (see the [Attention Pin Guide](https://dev.blues.io/guides-and-tutorials/notecard-guides/attention-pin-guide/)).
-- Check signal strength. Weak-signal retries add extra cellular sessions and burn more energy. Use [`card.wireless`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-wireless) in the blues.dev In-Browser Terminal to inspect RSSI and bars.
-- Motion events are accumulating more than once per day. Raise `motion_threshold` and increase `motion_bucket_sec` to reduce false triggers.
-
-**Event payload is missing a field (e.g., `cycle` is absent).**
-
-- Check the firmware version. Early versions may have had different payloads. Ensure you've uploaded the latest sketch and that the installed **Blues Wireless Notecard** library is up to date.
-
-## 11. Limitations and Next Steps
-
-This reference design is scoped to bench validation and limited field trials: the fastest path from "we should track these containers" to a live cellular uplink with site-level location. A few deliberate scope choices keep that path short; each is documented below alongside the production hardening that turns a 12–24 month LiPo prototype into a multi-year deployed fleet.
-
-### Simplified for the POC
-
-Each item below is a deliberate scope choice that keeps the prototype path short, paired with the production hardening that turns a bench build into a deployed fleet.
-
-**LiPo battery vs. Li-SOCl₂ is the primary scope constraint.** This POC build uses a rechargeable 2 Ah LiPo on the Notecarrier CX's charge-enabled power path. As discussed in [§9](#9-validation-and-testing), LiPo self-discharge limits practical service life to 12–24 months regardless of the low active-drain budget — this is the principal reason this build is documented as a bench prototype rather than a production deployment. For genuinely multi-year deployments — 3 to 5+ years between swaps — the right chemistry is Li-SOCl₂ (e.g., a D-size Tadiran TL-5930 at 3.6 V / 14.5 Ah, under 1% annual self-discharge). However, the Notecarrier CX's onboard charge IC is designed for LiPo chemistry; **connecting a Li-SOCl₂ primary cell directly to the JST `LIPO` port would engage the charge circuit against a non-rechargeable cell, which is unsafe.** A production deployment with Li-SOCl₂ requires a purpose-designed carrier board with no charging circuitry and a direct +VBAT input matched to the primary cell chemistry — the power path must be engineered from the ground up, not adapted from a charge-enabled design. The Notecarrier CX documented here is the right platform for prototyping and limited field trials.
-
-**Not rated for classified or explosive atmospheres.** This build uses standard commercial electronics and a LiPo battery — none of which carry ATEX, IECEx, Class I Division 2, or intrinsically safe certification. Deployments in classified or explosive atmospheres (including certain gas-cylinder and industrial-gas environments where flammable gases or vapors may be present) require a purpose-certified assembly: enclosure, battery, antenna, and all electronics must carry the appropriate hazardous-location rating. **That certification scope is well beyond this POC.**
-
-**Enclosure fit and gland rating** need verification when adapting the design. The main BOM specifies the Hammond 1554C2GY (NEMA 4X / IP67, polycarbonate) for field deployment; the Hammond 1591XXBSFLBK (IP54, ABS) is listed only for bench bring-up. When adapting to a different enclosure, verify the interior cavity against the Notecarrier CX footprint (76 × 38 mm board) and the flat LiPo pack before committing — inner dimensions tighter than ~95 × 50 mm will require the board and battery to be stacked rather than laid flat. Any cable gland through which a cellular antenna pigtail exits the enclosure must be rated to the same IP level as the enclosure body; the Essentra M16 IP68 gland in the BOM satisfies this for both the 1554C2GY and any IP68-rated replacement.
-
-**Triangulation accuracy** varies with the surrounding environment. Cell-tower and WiFi AP triangulation (`mode:"wifi,cell"`) is the default in this build. In AP-dense environments — warehouses, distribution centers, port terminals — the combined mode can improve accuracy to 15–200 m, more than sufficient to identify individual customer sites. In rural or outdoor areas with few APs the device falls back to cell-only, which delivers roughly 300 m to 5 km accuracy depending on tower density — still adequate for site-level identification in most cases. For applications requiring sub-50 m precision (identifying a specific dock door rather than a facility), a GPS fix via [`card.location.mode`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location-mode) `periodic` is the upgrade path, at the cost of a GPS antenna, cold-start latency of up to a few minutes, and additional power per fix.
-
-**No server-side geofencing.** The POC reports locations but does not evaluate them. Alerting when a container leaves an expected zone or appears at an unexpected site is best implemented as a [Notehub JSONata route transform](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — comparing `where_lat`/`where_lon` against fleet-level environment variables that define site polygons and firing a webhook when a container appears outside any known site, or in the downstream fleet application. Either approach allows site boundary data to change without a firmware update.
-
-**No transit-leg pairing.** The firmware reports `"departed"` and `"arrived"` events independently; correlating them into origin-destination legs (Container #4712 left Supplier A at 08:15 and arrived at Customer B at 14:42) requires logic in the fleet application, which has the context to make the association.
-
-**Single wake reason.** On a timer wake the firmware does not distinguish between "timer fired because heartbeat_hours elapsed" and "timer fired because the device reset"; both result in a heartbeat Note. A `card.time` call could confirm elapsed wall-clock time and refine the `reason` field, but at the cost of an additional I²C round trip on every timer wake.
-
-**No tamper or shock detection.** The accelerometer could detect sustained unusual orientations or sharp impact signatures. These are viable firmware extensions that this POC does not implement.
-
-### Production Next Steps
-
-Taking this prototype toward a multi-year deployed fleet means re-engineering the power path and layering on the cloud-side intelligence a real tracking operation needs.
-
-**A custom carrier board** with a direct +VBAT input (no charging circuitry) supports a D-cell Li-SOCl₂ primary, with the Notecarrier CX form factor used only for prototyping.
-
-**Voltage-variable sync** via [`hub.set voutbound/vinbound`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) lets the Notecard natively reduce cellular session frequency as the battery drains once `voutbound`/`vinbound` voltage thresholds are configured. The shipped firmware issues `hub.set` with fixed `outbound`/`inbound` only; enabling voltage-variable sync requires either (a) a small firmware change to add `voutbound`/`vinbound` fields to the `hub.set` call and verify those fields are not overwritten by the `hub.set` reissue that fires when `heartbeat_hours` changes, or (b) a one-time commissioning step in the blues.dev In-Browser Terminal to set the fields directly on each device.
-
-**Server-side geofencing** via Notehub JSONata compares `where_lat`/`where_lon` against fleet-level environment variables defining site polygons, firing a route to a webhook when a container appears outside any known site.
-
-**[Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** provides over-the-air host firmware updates to the entire fleet — essential for a tracker that may be affixed to a container for 5+ years without a hands-on service visit.
-
-**Per-container asset metadata** (container ID, type, tare weight, inspection due date) stored as Notehub device-level environment variables and appended to events by a JSONata route transform lets the fleet portal display rich container profiles without requiring the tracker to store or transmit that data itself.
-
-## 12. Summary
-
-For the pool manager who used to discover container shrinkage only at quarter-end, when the replacement budget caught up with reality, every container now reports back on its own schedule: a `tote_event.qo` Note within ~30 seconds of motion starting or stopping, and a `tote_heartbeat.qo` once a day even when the container sits perfectly still — each carrying battery state and a Notehub-resolved location appended automatically from cell-tower and WiFi AP triangulation. A tracker that clips to the container, wakes only when something interesting happens, and costs essentially nothing to operate between wakes is the right match for assets that spend most of their time sitting still: the Notecard's built-in accelerometer does the patient work of watching for motion at microamp power levels, and the Notecarrier CX's ATTN-gated host power keeps the Cygnet MCU genuinely off between wakes: not sleeping, off. Prepaid cellular means the same hardware and firmware deploys identically at an urban cold-storage facility and a rural distribution depot, with no IT coordination at either site. The data that comes out — where each container last checked in, whether it moved today, and whether its battery is healthy — is enough to run weekly reconciliations without manual cycle counts, flag containers that haven't moved in 30 days for recovery calls, and spot customers retaining containers past their agreed dwell time before it compounds into a shortage emergency.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/returnable-container-and-tote-pool-tracker/)**
diff --git a/74-returnable-container-tote-pool-tracker/banner.png b/74-returnable-container-tote-pool-tracker/banner.png
deleted file mode 100644
index 4b723b32..00000000
Binary files a/74-returnable-container-tote-pool-tracker/banner.png and /dev/null differ
diff --git a/74-returnable-container-tote-pool-tracker/diagrams/01-system-architecture.svg b/74-returnable-container-tote-pool-tracker/diagrams/01-system-architecture.svg
deleted file mode 100644
index 0fa4245c..00000000
--- a/74-returnable-container-tote-pool-tracker/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,76 +0,0 @@
-
diff --git a/74-returnable-container-tote-pool-tracker/diagrams/02-wiring-assembly.svg b/74-returnable-container-tote-pool-tracker/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 9b242e7a..00000000
--- a/74-returnable-container-tote-pool-tracker/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,91 +0,0 @@
-
diff --git a/74-returnable-container-tote-pool-tracker/diagrams/03-data-flow.svg b/74-returnable-container-tote-pool-tracker/diagrams/03-data-flow.svg
deleted file mode 100644
index 95a23176..00000000
--- a/74-returnable-container-tote-pool-tracker/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,78 +0,0 @@
-
diff --git a/75-heavy-equipment-hours-of-use-utilization-tracker/README.md b/75-heavy-equipment-hours-of-use-utilization-tracker/README.md
index 0a9f509a..bfdb598d 100644
--- a/75-heavy-equipment-hours-of-use-utilization-tracker/README.md
+++ b/75-heavy-equipment-hours-of-use-utilization-tracker/README.md
@@ -1,508 +1,5 @@
# Heavy Equipment Hours-of-Use & Utilization Tracker
-
+A retrofit for mobile heavy equipment that uses a magnetically mounted, solar-trickle-charged enclosure to detect engine on/off transitions via vibration, accumulating a software hour meter and reporting over cellular or satellite.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Heavy%20Equipment%20Hours-of-Use%20%26%20Utilization%20Tracker).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/equipment_hours_tracker/](firmware/equipment_hours_tracker/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a retrofit [asset location tracking](https://blues.com/solutions-location-tracking/) solution for mobile heavy equipment — excavators, generators, compactors, light towers, and any machine a rental company or OEM needs to bill by the hour and maintain on schedule. A magnetically mounted, solar-trickle-charged enclosure uses a 3-axis accelerometer to detect engine-on/off transitions via vibration signature, accumulates a persistent software hour meter, and reports location and utilization back to the [Blues Notehub](https://blues.com/notehub/) cloud service over cellular or satellite, with no wiring harness, no equipment modification, and no dependency on a job-site network. [Skylo](https://www.skylo.tech/)-supported satellite fallback keeps the device reporting from remote pipeline corridors, open-pit mines, and wind-farm construction zones where terrestrial coverage runs thin. The hardware is a Notecarrier CX with a Notecard for Skylo and an external IMU (see §4 for the BOM).
-
-## 1. Project Overview
-
-**The problem.** A piece of rental heavy equipment is a revenue-generating asset measured in hours. The excavator that a contractor rented on Monday morning needs an accurate engine-hour count for billing on Friday afternoon, a warranty-hours check before the next delivery, and a predictive-maintenance flag at 250-hour service intervals. Hardwired telematics (OBD-II interfaces, CAN-bus taps, hour-meter relays) work well on new fleet acquisitions but are expensive to retrofit on older machines, often require equipment downtime for installation, and occasionally void warranties when they involve accessing the engine control unit.
-
-Vibration-based hour detection solves the retrofit problem entirely. A small enclosure attached magnetically to the equipment frame asks one question on each sample: *is this equipment's engine currently running?* Getting the answer right is harder than it sounds. A rented excavator sitting in the back of a flatbed travels to a job site with its engine off, but the truck's diesel vibration and road shock look a lot like engine idle to a simple threshold-based accelerometer. This project addresses that with a two-parameter vibration signature algorithm: the root-mean-square (RMS) amplitude of the net acceleration residual measures activity level, and the coefficient of variation (CV, or σ/μ) discriminates steady periodic engine vibration from the irregular, bursty character of transport shock. Engine idle produces a low CV; road vibration produces a high one. Together they give three reliable states (**engine running**, **in transport**, and **idle/stopped**) without a single wire to the equipment.
-
-**Why Notecard for Skylo.** The equipment is mobile by definition, moving among customer job sites with no WiFi and often in areas where cellular coverage is marginal or absent. Open-pit mine sites, remote pipeline corridors, and offshore wind-farm construction zones all fall into this category. [Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) consolidates LTE-M, NB-IoT, GPRS, WiFi fallback, and Skylo satellite NTN (non-terrestrial network) on a single M.2 module: no separate satellite modem, no Starnote companion board required. When cellular is available, Notes flow over LTE-M with the low latency and high throughput you'd expect. When the equipment sits at the bottom of a quarry or behind a ridge where no tower reaches, the Notecard automatically falls back to satellite uplink through Skylo's NTN satellite service. The operator sees unbroken location and hours telemetry regardless of site topology, and the firmware never has to know which network was used.
-
-
-
-**Deployment scenario.** The enclosure mounts magnetically to any ferrous chassis surface — frame rail, tool-box lid, battery tray. No holes drilled, no wiring harness, no OEM cooperation. A small solar panel epoxied or bolted to the top of the enclosure trickle-charges a 2000 mAh LiPo through the Notecarrier CX's built-in solar charging circuit. The equipment can sit unused on a lot for weeks and the tracker will maintain charge; when the operator fires up the machine on a remote site, the vibration classifier detects the engine-start event and the Notecard ships the telemetry by whatever network is available.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Three pieces of work happen on the equipment itself, and they all have to fit into a 30-second wake budget. The Cygnet STM32 host on the Notecarrier CX comes up via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) host power gating, initializes the Adafruit LSM6DSOX accelerometer over I2C, and grabs a 2-second burst of 3-axis samples at 104 Hz. The vibration classifier turns that burst into one of three states (IDLE, RUNNING, or TRANSPORT), feeds the hour-meter accumulator, and compares the result against the previous wake. A state change fires an immediate event; an elapsed summary window fires a summary Note. Between wakes the host is fully powered off, and the Notecard holds the persisted state struct in its internal flash until the `ATTN` timer reapplies host power.
-
-**Notecard responsibilities.** Once the host hands off, Notecard for Skylo takes over the network side. It queues [Notes](https://dev.blues.io/api-reference/glossary/#note) locally and opens cellular or satellite sessions on two cadences configured in [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set): a daily outbound sync that carries queued summaries and an 8-hour inbound check-in (`inbound: 480`) that pulls environment-variable updates from Notehub. After each state-change event lands in the queue, the firmware issues a separate `hub.sync` call so the billing record doesn't wait for the next scheduled outbound window. Periodic GPS location sampling (every 15 minutes) and geofence configuration both run through [`card.location.mode`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location-mode), with a separate [`card.location.track`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location-track) call enabling the 4-hour heartbeat `_track.qo` record. If a geofence is configured through [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/), the Notecard evaluates it autonomously and emits a `_track.qo` event the moment the equipment leaves the job site; no host involvement is required after the fence parameters are applied.
-
-**Notehub responsibilities.** The Notecard's embedded global SIM handles cellular and Skylo NTN satellite sessions against supported carriers worldwide, delivering events to [Notehub](https://notehub.io) over the Internet; Notehub ingests, stores, and applies project-level routes from there. The operator never touches firmware to retune the fleet. Fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) let you adjust vibration thresholds and geofence parameters from the web console without a truck roll. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) segment devices by rental customer, equipment class, or geographic territory so routing and alerting can differ by group.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP Pub/Sub, Snowflake, and other destinations. State-change events (`equip_event.qo`) are good candidates to route to an on-call webhook or work-order system; daily summaries (`equip_summary.qo`) suit a time-series historian or billing database. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for configuration; this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-1. Create a [Notehub project](https://notehub.io) and copy your ProductUID.
-2. Wire the bench: Notecarrier CX + NOTE-NBGLWX + LSM6DSOX on I2C (see [§5](#5-wiring-and-assembly) for pinout).
-3. Edit `firmware/equipment_hours_tracker/equipment_hours_tracker_helpers.h` — replace the empty `#define PRODUCT_UID ""` with your project value (format: `com.your-company.your-name:tracker`).
-4. Install board core: `arduino-cli core install STMicroelectronics:stm32 --additional-urls https://raw.githubusercontent.com/stm32duino/BoardManagerFiles/main/STM32/package_stm_index.json`
-5. Compile and upload (see [§7.1](#71-installing-and-flashing) for full `arduino-cli` commands with your port).
-6. Open serial monitor at **115200 baud**. You should see `[BOOT] Cold boot` or `[VIB]` log lines every 30 seconds.
-7. Power the device. Open Notehub → your project → **Events** tab. You should see `_session.qo` within ~1 minute on cellular (longer on satellite depending on sky view). Tap the enclosure to trigger vibration; state-change events appear in `equip_event.qo`.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "equip_summary.qo",
- "body": {
- "run_h": 6.25,
- "run_h_total": 1253.75,
- "transport_h": 1.08,
- "bat_v": 4.07,
- "fault_ct": 0
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with embedded Cygnet STM32L4 host — solar/LiPo charging circuits, ATTN power-gating header, and I2C break-outs in one compact board. No separate MCU needed. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Single M.2 module combining LTE-M/NB-IoT/GPRS, WiFi, GPS/GNSS, and Skylo satellite NTN. Cellular-first with automatic satellite fallback. See [datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for regional coverage. Includes 500 MB cellular + 10 KB satellite data with the device. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter inline on the load rail (LiPo → `VBAT`) for ground-truth consumption measurement during bench bring-up; confirms the host sleep/wake pattern and whole-device energy draw. Measures load-side consumption only, not the solar charger input. Not required in production. |
-| [Adafruit LSM6DSOX 6-DoF IMU breakout (#4517)](https://www.adafruit.com/product/4517) | 1 | ST LSM6DSOX 3-axis accelerometer + gyroscope on I2C. Only the accelerometer axes are used; the gyroscope is explicitly shut down in firmware. STEMMA QT/Qwiic connector makes the bench wiring a single cable. |
-| [Adafruit 6V 1W solar panel (#3809)](https://www.adafruit.com/product/3809) | 1 | 6V output is within the Notecarrier CX solar input range (4.5–7V). 1W is sufficient for trickle-charging a 2000 mAh LiPo given ≥2–3 hours of daily sun exposure. |
-| 3.7V LiPo, 2000 mAh, JST-PH 2-pin (e.g. [Adafruit #2011](https://www.adafruit.com/product/2011)) | 1 | Single-cell LiPo for overnight and cloudy-day runtime. At the 65–120 mAh/day whole-device consumption estimated in §10 (which accounts for three daily inbound check-in sessions plus one outbound sync) and applying an 80% practical discharge depth, 2000 mAh provides approximately 13–24 days of operation with zero solar input at the default 30-second sample cadence. Actual runtime depends on network type (satellite draws more than LTE-M), signal quality, and GNSS fix success rate — validate with Mojo for your specific site. |
-| [K&J Magnetics MM-C-36](https://www.kjmagnetics.com/mm-c-36-neodymium-male-stud-mounting-magnet) neodymium pot magnet + [MM-RC-36 rubber cover](https://www.kjmagnetics.com/proddetail.asp?prod=MM-RC-36) | 1 | 36 mm face, M6 × 1.0 external threaded stud, 41 kg (90 lb) pull force — well above the ~250–350 g assembled enclosure weight. The nickel-coated steel cup concentrates flux; the MM-RC-36 silicone over-cap (~1 mm thick) protects the chassis finish and reduces sliding. Max continuous operating temperature: 80 °C — suitable for frame rails and battery trays; avoid mounting on exhaust or turbocharger surfaces. **Regional alternative:** any rubber-coated 36–42 mm M6-stud neodymium pot magnet rated ≥15 kg from RS Components or Grainger is a direct substitute. |
-| IP67 weatherproof enclosure, ~130 × 80 × 55 mm (e.g. Hammond 1554B2GY) | 1 | Polycarbonate / ABS housing protects electronics against rain, jet wash, and splashing. GPS signals pass through the plastic lid, so the GPS antenna can be mounted inside. Sized to fit Notecarrier CX, LiPo, and cable management. |
-| Passive GPS/GNSS patch antenna, u.FL connector (e.g. [Adafruit #2460](https://www.adafruit.com/product/2460)) | 1 | Attaches to the NOTE-NBGLWX `GPS` u.FL port. **Must be passive** — the GPS port does not supply DC bias, and the datasheet explicitly requires a passive antenna. The 50 mm pigtail is long enough to position the 9 mm × 9 mm ceramic patch against the inside face of the polycarbonate lid; GPS L1 signals penetrate the plastic without requiring a cable gland. For best fix rate, orient the patch toward the sky. |
-| u.FL-to-SMA female pigtail cable, ~150 mm (e.g. [Adafruit #851](https://www.adafruit.com/product/851)) | 1 | Routes the NOTE-NBGLWX `MAIN` u.FL port through the enclosure wall to the Skylo-certified MAIN antenna. The u.FL end (**female**, clips onto the Notecard's `MAIN` port) attaches inside the enclosure; the other end is an **SMA female jack (panel-mount style)** — pass it through a drilled or punched hole in the enclosure sidewall, secure with the hex nut, and apply a thin bead of silicone sealant under the nut face to maintain the enclosure's weatherproof rating at the penetration. The Skylo-certified MAIN antenna's **SMA male plug** screws directly onto this SMA female from outside the enclosure — no separate bulkhead adapter is needed. The `MAIN` port carries both LTE-M/NB-IoT/GPRS cellular **and** Skylo NTN satellite on a single antenna path (S-Band / L-Band, bands B23/B255/B256). **The Skylo-certified MAIN antenna included with the NOTE-NBGLWX must be used on this port** — substituting a different antenna removes Skylo certification and may result in network blocking by Skylo. Mount the MAIN antenna outside the enclosure with an unobstructed sky view. See the [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for the full antenna requirements. |
-
-Notecard for Skylo ships with a factory-provisioned SIM, global cellular and satellite service, 500 MB of cellular data, 10 KB of satellite data, and 10 years of connectivity included in the device price: no activation fees, no monthly minimum. This entitlement applies to Notecard for Skylo; the Notecarrier CX and Mojo are hardware-only items with no attached connectivity service.
-
-## 5. Wiring and Assembly
-
-
-
-Notecard for Skylo seats into the Notecarrier CX's M.2 Key E slot. The Adafruit LSM6DSOX connects via the Notecarrier CX's I2C header; a Qwiic/STEMMA QT cable makes this a single two-connector snap if you own the matching Qwiic cable, otherwise use four discrete wires. The Mojo sits inline on the main power feed during bench validation.
-
-Pin-by-pin:
-
-- **+3V3** → LSM6DSOX `VCC` (3.3 V logic; do not use 5V, the sensor is 3.3V only)
-- **GND** → LSM6DSOX `GND`
-- **SDA** → LSM6DSOX `SDA` (the Notecarrier CX has 4.7 kΩ pull-ups on-board)
-- **SCL** → LSM6DSOX `SCL`
-- **Solar JST** → 6V solar panel (observe polarity marked on connector; red = positive)
-- **LiPo JST** → 3.7V 2000 mAh LiPo cell (JST-PH 2-pin, same polarity convention)
-- **ATTN → EN jumper (REQUIRED for host power-gating).** On Notecarrier CX, `ATTN` (the Notecard's configurable interrupt output) and `EN` (the input that gates the carrier's host 3.3 V rail to the Cygnet) are exposed as **separate** pins on the dual 16-pin header. See the [Notecarrier CX datasheet header description](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/#notecarrier-cx-dual-16-pin-headers). They are **not** connected internally. To make `card.attn mode:sleep` actually cut power to the Cygnet, run a short jumper wire from the `ATTN` header pin to the `EN` header pin. With that wire in place, when `goToSleep()` issues `card.attn mode:sleep`, the Notecard drives `ATTN` low → `EN` low → the host 3.3 V rail collapses and the Cygnet powers off. `SAMPLE_INTERVAL_SEC` later the Notecard reasserts `ATTN` high → `EN` high → the Cygnet powers up and re-enters `setup()`. Without the `ATTN→EN` jumper the host runs continuously between samples; the firmware's `loop()` fall-back will keep functional sampling working, but baseline current will be ~5–10 mA instead of the ~20–60 µA target, and the solar/battery budget in §10 will not hold. Verify the jumper is functioning by confirming the Cygnet's serial output stops during the sleep window. See the [`card.attn` API reference](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) and the [Feather MCU low-power guide](https://dev.blues.io/guides-and-tutorials/notecard-guides/feather-mcu-low-power-management/) for additional context on this pattern.
-
-**Antenna placement.** The NOTE-NBGLWX has exactly two u.FL antenna ports:
-
-- **`MAIN` u.FL** — carries both LTE-M/NB-IoT/GPRS cellular and Skylo NTN satellite on a single path. Clip the u.FL end of the pigtail (Adafruit #851 or equivalent u.FL-to-SMA-female-jack cable) onto the `MAIN` u.FL port, then route the cable to a drilled or punched hole in the enclosure sidewall. Pass the pigtail's **SMA female jack** (panel-mount end) through the hole from inside, thread on the hex nut from outside to clamp it against the wall, and apply a thin bead of silicone sealant under the nut face; this is the weatherproof seal at the RF penetration. Screw the **SMA male plug** of the Skylo-certified MAIN antenna included with the NOTE-NBGLWX onto the exposed SMA female from outside the enclosure, and orient the antenna pointing upward with a clear, unobstructed sky view. Skylo NTN requires the antenna to be outdoors; do not mount under steel overhead panels or inside equipment compartments.
-- **`GPS` u.FL** — passive GNSS only; do not connect an active (bias-powered) antenna. Attach the Adafruit #2460 passive GPS patch here and position it against the inside face of the polycarbonate lid. GPS L1 signals penetrate the plastic without a cable gland.
-
-The MAIN antenna is Skylo-certified as a unit with the NOTE-NBGLWX; do not substitute a different antenna or add extension cables beyond the short u.FL-to-SMA-female pigtail used to bring the MAIN port through the enclosure wall.
-
-**Mojo placement (bench only).** For Mojo bench measurements, Mojo **replaces** the normal battery feed for the duration of the test; it does not splice in alongside the LiPo. Before inserting Mojo, **disconnect the LiPo from the Notecarrier CX's LiPo JST**. With the LiPo JST unplugged, route power through the Mojo: bench supply (or the LiPo brought out through a bare lead) → Mojo `BAT` input → Mojo `LOAD` output → Notecarrier CX `VBAT` pad. Leaving the battery on the JST while also feeding `VBAT` through the Mojo creates a parallel power path that bypasses the intended measurement point and risks backfeed through the charger circuit. Also disconnect the solar panel; the solar input is a separate rail that will offset the load draw if left connected.
-
-**Accelerometer orientation.** The LSM6DSOX can be mounted in any orientation; the firmware computes the combined 3-axis magnitude and subtracts the 1g gravity baseline. No axis alignment is required. The one practical consideration is rigidity: mount the breakout board solidly to the enclosure interior (hot-glue or standoffs) so it vibrates with the equipment chassis rather than floating on its wires.
-
-**Mounting safety.** Heavy equipment operates in demanding environments; treat the magnetic mount as a primary attachment that requires supplemental protection for field deployment:
-
-- **Secondary retention.** Add a safety tether — a short loop of steel wire or nylon strap tied between an M6 bolt on the enclosure and a nearby chassis fixture. If the magnet loses adhesion on a painted or lightly-corroded surface, the tether catches the enclosure before it falls into moving linkages or undercarriage.
-- **Surface temperature.** Neodymium magnets begin to lose pull force above ~80 °C and can irreversibly demagnetize above ~150 °C. Do not mount on exhaust manifolds, muffler housings, turbocharger bodies, or any chassis surface that radiates heat from the engine bay. Frame rails, battery trays, and tool-box lids are typically within safe temperature range.
-- **Moving parts and pinch zones.** Avoid areas near pivot pins, hydraulic cylinder clevises, boom linkages, and undercarriage rails where the enclosure could be crushed or snagged during machine articulation.
-- **Washdown exposure.** The enclosure must be IP67-rated with all cable glands properly seated and the lid gasket undamaged. Pressure-washer cleaning (common on construction sites) can exceed IP67 limits if aimed directly at a gland; orient glands away from the primary washdown direction.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) (format: `com.your-company.your-name:equipment-tracker`).
-
-2. **Set `PRODUCT_UID` in firmware.** Open [`equipment_hours_tracker_helpers.h`](firmware/equipment_hours_tracker/equipment_hours_tracker_helpers.h) and replace the empty string on the `#define PRODUCT_UID ""` line with your value. Alternatively, pass it as a build flag: `-DPRODUCT_UID=\"com.your-company:tracker\"`.
-
-3. **Claim the Notecard.** Power the assembled unit. On first cellular or satellite contact the Notecard claims itself to your project automatically, with no manual step. It will appear in the **Devices** tab within a few minutes on cellular; allow additional time on NTN while the Notecard establishes a satellite session, since Skylo availability depends on sky visibility and signal conditions, not orbital pass timing.
-
-4. **Create a Fleet.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. The natural units here are *rental customer*, *equipment class*, and *geographic territory* — a fleet for compact equipment, a fleet for large iron, or a fleet per region. [Smart Fleet rules](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) support device-tag-based auto-assignment; apply tags in Notehub to route units into the appropriate customer or site fleet as their assignments change.
-
-5. **Set environment variables.** In Notehub, navigate to **Fleet → Environment** (or **Device → Environment** for per-unit override). All variables are optional; firmware defaults are shown. The device pulls them on the next inbound sync (default every 8 hours); no reflash needed.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `vib_run_mg` | `15.0` | RMS activity threshold in milli-g. Values below this are classified as IDLE regardless of CV. Lower on smooth engines; raise in high-vibration environments to avoid false positives. |
- | `vib_cv_max` | `0.40` | Coefficient of variation ceiling for "engine running." CV below this value (steady vibration) → RUNNING. CV above (bursty vibration) → TRANSPORT. Typical engine-idle CV is 0.1–0.25; truck-bed bounce CV is 0.5–1.0+. |
- | `summary_interval_min` | `1440` | Minutes between daily summary Notes. Changing this also re-applies `hub.set outbound` so the Notecard's sync cadence matches. Minimum enforced value: 60 min. |
- | `geofence_lat` | `0.0` | Latitude of the job-site geofence center (decimal degrees, –90 to 90). **Must be set together with `geofence_lon` and `geofence_radius_m`.** The default 0.0 is treated as "not configured"; setting a non-zero radius while leaving lat/lon at the 0,0 default will not activate geofencing — the firmware requires all three parameters to be non-zero and in range before applying the fence. |
- | `geofence_lon` | `0.0` | Longitude of the job-site geofence center (decimal degrees, –180 to 180). Must be set together with `geofence_lat` and `geofence_radius_m`. |
- | `geofence_radius_m` | `0` | Radius in meters. When all three geofence parameters (`geofence_lat`, `geofence_lon`, `geofence_radius_m`) are non-zero and in range, the Notecard monitors the device's position against the fence center and triggers a `_track.qo` event when the equipment leaves the site. A change to any of the three values (including radius alone) is detected on the next device wake and re-applied. Setting to 0 clears an active fence on the next wake. No-op if no fence was previously configured. |
-
-6. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `equip_event.qo` (state-change events, low volume, immediate delivery to billing or dispatch) and a second for `equip_summary.qo` (rolling summary window Notes, delivered to a utilization dashboard or historian). A third route on `_track.qo` handles geofence-exit location breadcrumbs. Separating the Notefiles at source means different routing urgencies without any filter logic in the route itself.
-
-### What to expect in Notehub
-
-- **`_session.qo`** — automatic Notecard housekeeping on each cellular or satellite session. Presence confirms the radio is reaching Notehub. Absence (combined with LED activity on the Notecard) usually points to a `PRODUCT_UID` mismatch or coverage gap.
-- **`equip_event.qo`** — one per state transition, transmitted immediately. The `event` field encodes the full from→to transition, not just the new state:
-
- | Transition | `event` tag | `session_min` |
- |---|---|---|
- | IDLE / TRANSPORT → RUNNING | `engine_start` | 0 |
- | RUNNING → TRANSPORT | `transport_start` | **non-zero** — duration of the engine run that preceded transport |
- | IDLE → TRANSPORT | `transport_start` | 0 |
- | RUNNING → IDLE | `engine_stop` | non-zero — duration of the run that just ended |
- | TRANSPORT → IDLE | `transport_stop` | 0 |
-
- Example `engine_stop` body (one form of billing record. See also `transport_start` with `session_min > 0` for RUNNING→TRANSPORT):
- ```json
- {
- "event": "engine_stop",
- "session_min": 94.5,
- "run_h_total": 1253.75,
- "epoch": 1746023400
- }
- ```
-- **`equip_summary.qo`** — one per `summary_interval_min` (default 1440 minutes), representing a rolling window from the last report, not a calendar day. The first summary after boot may cover a shorter window if the Notecard's clock was not yet valid at startup. Body:
- ```json
- {
- "run_h": 6.2,
- "run_h_total": 1253.7,
- "transport_h": 1.1,
- "bat_v": 4.07,
- "fault_ct": 0
- }
- ```
- `bat_v` below ~3.5V is a low-battery warning. `transport_h` provides a secondary utilization metric: time spent moving between sites. `fault_ct` is the number of state-change events dropped due to event-queue overflow since the last summary; a non-zero value indicates the Notecard was unreachable for multiple consecutive wakes.
-- **`_track.qo`** — automatic Notecard location heartbeat (every 4 hours) and geofence events. Not generated by firmware code; the Notecard's GPS subsystem owns these. Example geofence-exit event (when equipment leaves the configured job site):
- ```json
- {
- "when": 1746023400,
- "location": {
- "lat": 37.7749,
- "lon": -122.4194,
- "accuracy": 48
- },
- "tower": true,
- "type": "geofence_exit",
- "status": "success"
- }
- ```
- Key fields: `type` distinguishes geofence-exit events from routine heartbeats; `location` carries coordinates and GPS accuracy (meters); `when` is the Unix timestamp. Use the `type` and timestamp fields in a Notehub route to trigger alerts when equipment leaves the fence.
-
-## 7. Firmware Design
-
-The firmware is split across three files that must reside together in the same Arduino sketch folder:
-
-- [`equipment_hours_tracker.ino`](firmware/equipment_hours_tracker/equipment_hours_tracker.ino) — `setup()` / `loop()` entry points and per-wake sequencing.
-- `equipment_hours_tracker_helpers.h` — type definitions, constants, and function prototypes.
-- `equipment_hours_tracker_helpers.cpp` — all helper-function implementations.
-
-Arduino build tooling automatically compiles every `.ino`, `.h`, and `.cpp` file in the sketch folder together; no manual include path or Makefile is required.
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Install via the Arduino Boards Manager (search "STM32 MCU based boards") and select **Blues Cygnet** as the board target.
-- **`Blues Wireless Notecard`** — [`note-arduino`](https://github.com/blues/note-arduino). Install via the Arduino Library Manager (`arduino-cli lib install "Blues Wireless Notecard"`), or select the latest stable version in the IDE Library Manager. See [note-arduino releases](https://github.com/blues/note-arduino/releases) for available versions.
-- **`Adafruit LSM6DS`** — install via Library Manager (`arduino-cli lib install "Adafruit LSM6DS"`). Also installs the `Adafruit Unified Sensor` dependency if not already present.
-
-**Flashing via Arduino IDE:** open `equipment_hours_tracker.ino`, select the **Blues Cygnet** board (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`), and click **Upload**. The Notecarrier CX presents the ST-Link interface on the same USB cable — no external programmer required.
-
-**Flashing via `arduino-cli`:**
-```bash
-# One-time: install the STM32 board core
-arduino-cli core install STMicroelectronics:stm32 \
- --additional-urls https://raw.githubusercontent.com/stm32duino/BoardManagerFiles/main/STM32/package_stm_index.json
-
-# Find the FQBN for the Cygnet variant on your installed core
-arduino-cli board listall | grep -i cygnet
-
-# Find your device's USB port
-arduino-cli board list
-
-# Compile and upload (replace port with your value from 'board list' above)
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- firmware/equipment_hours_tracker/
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/cu.usbmodem* \
- firmware/equipment_hours_tracker/
-```
-Replace `/dev/cu.usbmodem*` with the port shown by `arduino-cli board list`. On Windows this is typically `COMx`, on Linux `/dev/ttyACM*`. The Notecarrier CX presents the device over a single USB cable (the ST-Link programmer is built-in).
-
-Open the serial monitor at **115200 baud** after flashing. You will see `[BOOT] Cold boot` on first power-on, then `[VIB]` lines on each 30-second wake showing the RMS and CV readings in real time.
-
-### 7.2 Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard one-time initialization (`hub.set`, `card.location.*`, template registration) | `notecardConfigure`, `defineTemplates` |
-| Environment-variable fetch and hub.set re-apply | `fetchEnvOverrides` |
-| Geofence reconfiguration on env-var change | `applyGeofenceIfChanged` |
-| Accelerometer burst sampling + RMS/CV classifier | `classifyVibration` |
-| Hour-meter accumulation per state bucket | `updateHourAccumulator` |
-| State-change event dequeue and delivery (Note.add + hub.sync, at-least-once retry) | `sendNextPendingEvent` |
-| Daily summary emission | `sendSummary` |
-| Time and voltage from Notecard | `getEpoch`, `getBatteryVoltage` |
-| Persist state to Notecard flash + sleep | `goToSleep` / `NotePayloadSaveAndSleep` |
-
-### 7.3 Sensor reading strategy
-
-Each wake, the host collects **208 accelerometer samples at 104 Hz** (approximately 2 seconds of data) from the LSM6DSOX. For each sample, it computes the 3-axis vector magnitude in m/s² and subtracts the 1g gravity baseline (~9.806 m/s²) to obtain the net dynamic acceleration. This residual is expressed in milli-g for threshold comparison.
-
-Two statistics are then computed over the 208-sample window:
-
-- **RMS** — the root-mean-square of the residual magnitudes. Low RMS means the equipment is stationary and undisturbed. High RMS means energy is present in the measurement.
-- **CV** (coefficient of variation, σ/μ) — the standard deviation normalized by the mean. A diesel engine idling at ~700 RPM generates ~11.7 Hz periodic vibration — repetitive, amplitude-stable, low CV (typically 0.10–0.25). Road shock from a truck chassis is aperiodic, with spikes at pothole crossings and relative quiet between: high CV (typically 0.50–1.0+). This is the discriminator that separates the two high-activity states.
-
-Classifying engine vibration from transport vibration was the primary design challenge here. A simple amplitude threshold would fire on both; CV is the second dimension that makes the problem tractable without training data or a heavy signal-processing stack.
-
-### 7.4 Event payload design
-
-Both Notefiles use [compact templates](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates), required for Notecard for Skylo, where satellite data packets are constrained to 256 bytes and cost per byte beyond the included 10 KB. Compact templates store Notes as fixed-length binary records on the Notecard rather than free-form JSON, reducing wire size 3–5× compared to untemplated Notes.
-
-`equip_summary.qo` (rolling summary window, queued):
-```json
-{
- "file": "equip_summary.qo",
- "body": {
- "run_h": 6.25,
- "run_h_total": 1253.75,
- "transport_h": 1.08,
- "bat_v": 4.07,
- "fault_ct": 0
- }
-}
-```
-
-`equip_event.qo` (queued on state transition; `hub.sync` issued separately for prompt delivery):
-```json
-{
- "file": "equip_event.qo",
- "body": {
- "event": "engine_stop",
- "session_min": 94.5,
- "run_h_total": 1253.75,
- "epoch": 1746023400
- }
-}
-```
-
-GPS coordinates are automatically attached to both Note types by the Notecard from its last valid GPS fix, declared in the compact templates using the `_lat`/`_lon` metadata keywords. No firmware code explicitly manages coordinates in the Note bodies.
-
-### 7.5 Low-power strategy
-
-Power efficiency matters for a solar-trickle-charged deployment. Three levers are pulled:
-
-1. **Host off between samples.** `NotePayloadSaveAndSleep` serializes the `PersistState` struct into Notecard flash, then issues a `card.attn` sleep command. With the `ATTN → EN` jumper described in §5 in place, the Notecard's `ATTN` line drives the Notecarrier CX `EN` input low, collapsing the host 3.3 V rail and powering the Cygnet off. The host consumes essentially zero current between wakes. On wake, `NotePayloadRetrieveAfterSleep` rehydrates the struct.
-
-2. **Gyroscope shut down.** `sox.setGyroDataRate(LSM6DS_RATE_SHUTDOWN)` turns off the gyroscope immediately after init; it's not needed for this application and saves ~0.5 mA during the 2-second sampling window.
-
-3. **Notecard sync decoupled from samples.** The Notecard runs in `periodic` mode with a daily outbound sync. After each state-change event Note is accepted by the Notecard, the firmware issues a separate `hub.sync` call to request prompt delivery outside the scheduled outbound window (typically 2–4 additional sessions per work day). Summaries queue and transmit in the daily outbound session. The firmware also configures an 8-hour inbound cadence (`inbound: 480`) so the Notecard checks Notehub for environment-variable updates three times per day. At default settings this produces approximately **4 radio sessions per day at minimum** (1 outbound + 3 inbound), plus an additional session per state-change event.
-
-4. **GPS fix cadence.** The firmware configures `card.location.mode` in `periodic` mode with a 900-second (15-minute) interval. In the typical deployment case, GNSS fix attempts occur on or around the 15-minute cadence when the Notecard determines a new fix is warranted. Each fix attempt typically draws 20–50 mA for 10–60 seconds; at up to 96 attempts per day this can contribute roughly **15–30 mAh/day** to the power budget, a meaningful fraction of the total. If solar input is marginal or the device is frequently stationary, increase `GPS_PERIOD_SECONDS` to reduce GNSS power consumption.
-
-Notecard for Skylo idles at approximately **8–18 µA @ 5V** between sessions (see the [low-power firmware design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)). LTE-M sessions for a small queued payload run on the order of tens of seconds at ~100–300 mA peak. Satellite (Skylo NTN) sessions measured in the Blues low-power guide consume approximately **27 mAh per 12-hour period with hourly syncs**; daily sync cadence will be materially lower. Expect the bench-measured total for a device with default settings to be approximately **65–120 mAh per 24 hours** depending on network type, signal quality, state-change event frequency, and GNSS fix success rate.
-
-### 7.6 Retry and error handling
-
-- The first `hub.set` call uses `sendRequestWithRetry(req, 10)` to handle the known cold-boot I2C race where the Notecard's I2C peripheral isn't ready for transactions immediately after power-on.
-- If `sox.begin_I2C()` fails (unplugged or miswired accelerometer), the firmware calls `goToSleep()` immediately rather than running with a broken sensor. The issue will appear in the serial monitor and on the next wake will be retried.
-- Both `card.time` and `card.voltage` responses check the `err` field before trusting the returned value. `getEpoch()` returns 0 if the Notecard has no valid time yet (no cellular/GPS sync), and all epoch-dependent logic gates on `now > 0`. `getBatteryVoltage()` returns 0.0 on an error response, which the summary Note will carry as a sentinel distinguishable from a healthy ~3.6–4.2 V reading.
-- The `summary_interval_min` env-var change path includes a minimum-value clamp (60 minutes) to prevent operators from accidentally configuring a 1-minute summary that would exhaust the satellite data budget in hours.
-- State-change events are not de-duplicated: if the classifier oscillates between RUNNING and TRANSPORT on rough terrain, each transition fires. If this produces alarm fatigue in a specific deployment, add a minimum-dwell counter (e.g., require 3 consecutive matching classifications before accepting a new state) as a production tuning step.
-
-### 7.7 Key code snippet 1: vibration classifier
-
-The CV threshold is what makes the engine-vs-transport discrimination work. The 2-second burst at 104 Hz is fast enough to capture multiple engine-combustion cycles (a 700 RPM diesel fires every ~0.086 seconds; 208 samples at 9.6 milliseconds spacing span ~26 combustion events).
-
-```cpp
-float mean = sum / VIB_SAMPLE_COUNT;
-float variance = (sum_sq / VIB_SAMPLE_COUNT) - (mean * mean);
-float stddev = (variance > 0.0f) ? sqrtf(variance) : 0.0f;
-float cv = (mean > 1.0f) ? (stddev / mean) : 1.0f;
-float rms = sqrtf(sum_sq / VIB_SAMPLE_COUNT);
-
-if (rms < g_vib_run_mg) return ST_IDLE;
-return (cv < g_vib_cv_max) ? ST_RUNNING : ST_TRANSPORT;
-```
-
-### 7.8 Key code snippet 2: compact template with GPS metadata
-
-The `_lat`/`_lon` keywords in a compact template body instruct the Notecard to embed the most recent GPS fix into the Note automatically. No explicit coordinate plumbing in `note.add` calls.
-
-Compact template value encoding (the numeric code parameter):
-- **14.1** = IEEE 754 4-byte float (32-bit), 1 decimal place in display. Use for large dynamic ranges (hours, totals, coordinates).
-- **12.1** = 2-byte signed float (16-bit), 1 decimal place in display. Use for smaller ranges with moderate precision (voltage, relative measurements).
-- **12** = 2-byte signed integer (16-bit), no decimal. Use for counters, small integers, or flags.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "equip_summary.qo");
-JAddNumberToObject(req, "port", 50);
-JAddStringToObject(req, "format", "compact");
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "run_h", 14.1); // 4-byte float, hours this window
-JAddNumberToObject(body, "run_h_total", 14.1); // 4-byte float, lifetime total hours
-JAddNumberToObject(body, "transport_h", 14.1); // 4-byte float, transport hours this window
-JAddNumberToObject(body, "bat_v", 12.1); // 2-byte float, battery voltage
-JAddNumberToObject(body, "fault_ct", 12); // 2-byte int, event-queue overflow counter
-JAddNumberToObject(body, "_lat", 14.1); // 4-byte float, auto-populated by Notecard
-JAddNumberToObject(body, "_lon", 14.1); // 4-byte float, auto-populated by Notecard
-notecard.sendRequest(req);
-```
-
-See [Notecard compact template documentation](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) for the complete list of format codes and their ranges.
-
-### 7.9 Key code snippet 3: immediate event with session duration
-
-The event tag is derived from **both** the previous and new state, not just the new state alone. This ensures `engine_stop` is reserved for transitions out of RUNNING (the billing record), while `transport_stop` marks the end of a transport leg:
-
-```cpp
-const char *tag;
-if (new_state == ST_RUNNING) tag = "engine_start";
-else if (new_state == ST_TRANSPORT) tag = "transport_start";
-else /* ST_IDLE */ tag = (g_s.prev_state == ST_RUNNING) ? "engine_stop"
- : "transport_stop";
-```
-
-`session_min` is non-zero on `engine_stop` (and on `transport_start` when transitioning from RUNNING) — it records how long the engine was running since the last `engine_start`. After being consumed by the first non-RUNNING transition, `run_session_start` is cleared to zero; subsequent non-RUNNING transitions (`transport_stop`, or any transition from a non-RUNNING prior state such as `IDLE → TRANSPORT`) therefore always emit `session_min = 0`.
-
-The Note is queued with `note.add` (without `sync:true`); a separate `hub.sync` call then requests prompt delivery without coupling the network session to the Note acknowledgement:
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "equip_event.qo");
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "event", "engine_stop");
-JAddNumberToObject(body, "session_min", session_min);
-JAddNumberToObject(body, "run_h_total", g_s.run_h_total);
-JAddNumberToObject(body, "epoch", (JNUMBER)epoch); // Unix timestamp of the transition
-notecard.sendRequest(req);
-
-// Separate hub.sync requests prompt delivery without coupling it to
-// the note.add acknowledgement.
-notecard.sendRequest(notecard.newRequest("hub.sync"));
-```
-
-## 8. Data Flow
-
-
-
-**Collected** on each 30-second wake: per-sample 3-axis acceleration magnitudes at 104 Hz for 2 seconds → RMS and CV → one of three equipment states: IDLE, RUNNING, TRANSPORT.
-
-**Accumulated** in flash: running hours today, lifetime running hours, transport hours today, session start timestamp.
-
-**Transmitted:**
-- `equip_event.qo` — one Note per state transition; after the Notecard acknowledges the queued Note, the firmware issues a `hub.sync` request to prompt delivery outside the scheduled outbound window. Typically 2–6 Notes per work day (engine start, possible midday idle, engine stop; transport start/stop on delivery days). Goes to Notehub within a cellular session-establishment window (~15–60 seconds), or when NTN service is available — satellite delivery depends on sky visibility and session establishment and may take longer than cellular.
-- `equip_summary.qo` — one Note per `summary_interval_min` (default 1440 minutes), queued and shipped in the Notecard's next outbound session. Covers the rolling summary window since the last report, not a calendar day; the first Note after boot may represent a partial window if the Notecard's clock was not yet valid at startup. Carries run hours for the window, lifetime total, transport hours, battery voltage, and a fault counter (`fault_ct`) reflecting any event-queue overflows since the last summary.
-- `_track.qo` — emitted autonomously by the Notecard's GPS subsystem every 4 hours as a heartbeat location record, and on geofence exit if `geofence_radius_m` is set. The firmware does not generate these directly.
-
-**Routed.** Both application Notefiles go to Notehub and from there to whatever downstream endpoints the project's routes specify. Typical fan-out: `equip_event.qo` → billing/dispatch system or CMMS (computerized maintenance management system) webhook; `equip_summary.qo` → time-series database for trending and predictive maintenance scheduling; `_track.qo` → mapping/GIS layer.
-
-**Alert triggers:**
-- `engine_start` — immediately actionable for rental companies tracking unauthorized after-hours use.
-- Any `equip_event.qo` with `session_min > 0` — a run session has just closed. The `event` tag distinguishes the state that followed: `engine_stop` means the machine ran then went idle; `transport_start` means it ran then started moving (RUNNING → TRANSPORT). Both carry the run duration in `session_min`. Billing systems must key off `session_min > 0` across **both** tags — keying only off `engine_stop` will miss completed run sessions that end in transport.
-- `transport_start` — equipment moving; useful for confirming scheduled deliveries or detecting unplanned moves. When `session_min` is non-zero, the engine was running immediately before transport began.
-- `transport_stop` — transit leg ended; combined with the preceding `transport_start` timestamp, gives transit duration for dispatch and mileage tracking.
-- Battery voltage below 3.5V in `equip_summary.qo` — indicates solar input is inadequate for the deployment site; suggest repositioning panel or adding capacity.
-- Absence of `equip_event.qo` for multiple days combined with `_track.qo` showing stable position — equipment may be idle on lot; candidate for redeployment or servicing.
-
-## 9. Validation and Testing
-
-**Expected steady-state on an active job site.** In normal operation, expect 2–4 `equip_event.qo` Notes per day (engine start, engine stop, possibly a midday shutdown), one `equip_summary.qo` per summary window (default every 24 hours), and six `_track.qo` location heartbeats per day (one every 4 hours, matching `GPS_HEARTBEAT_HOURS = 4`), plus any additional `_track.qo` Notes triggered by geofence events. On a delivery day, expect additional `transport_start` and `transport_stop` events bracketing the transit. If the engine was running immediately before transport, the `transport_start` Note will carry a non-zero `session_min` recording the run duration.
-
-**Bench validation.** On a desk, the equipment is IDLE; the accelerometer should report low RMS and the classifier should output `ST_IDLE`. The serial monitor shows `[VIB] rms=X.X mg cv=Y.YYY → IDLE`. To simulate an engine:
-
-- Tap the enclosure rhythmically against a surface at ~10 Hz (not perfectly on beat, vary the interval slightly to mimic engine combustion irregularity). The CV should drop below 0.40 and the classifier should output `ST_RUNNING`.
-- Shake the enclosure sharply then randomly (slap → pause → slap → pause). This models road bounce: high CV, `ST_TRANSPORT`.
-
-**Tuning the classifier on your equipment.** If the classifier misfires at default thresholds (false RUNNING on transport, or failure to detect idle):
-
-1. Tap the enclosure in the three states and note the RMS and CV values printed on the serial monitor.
-2. If **idle is triggering as RUNNING**, raise `vib_run_mg` slightly (try 18.0 or 20.0). This raises the activity floor.
-3. If **transport is triggering as RUNNING**, lower `vib_cv_max` (try 0.35 or 0.30). This tightens the "engine-like vibration" criterion.
-4. If **engine start is missed entirely**, lower `vib_run_mg` (try 12.0 or 10.0) or raise `vib_cv_max` (try 0.45).
-
-Update the environment variables in Notehub (**Fleet → Environment**), and the device will pull the new thresholds on the next inbound sync (~8 hours by default, or immediately if you trigger a manual sync in Notehub). Confirm the new values appear in the serial output on the next wake — the firmware logs `[ENV] vib_run_mg=X.X vib_cv_max=Y.YY`. Iterate with live equipment until classification is reliable for your specific engine type and chassis.
-
-**Power validation with Mojo.** The [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) reports cumulative mAh over its Qwiic link. For bench measurements, Mojo replaces the normal battery feed: **leave the LiPo JST unplugged** and power the carrier's `VBAT` pad exclusively through the Mojo — bench supply (or the LiPo with its JST lead brought out directly) → Mojo `BAT` input → Mojo `LOAD` output → Notecarrier CX `VBAT` pad. **Also disconnect the solar panel** — the Notecarrier CX's solar charger input is a separate rail; leaving it connected during bench testing means solar current offsets the load draw, understating actual consumption and making the measurement non-repeatable across different light conditions. With both the LiPo JST and solar panel disconnected and power flowing exclusively through the Mojo, all load current passes through the Mojo's coulomb counter and the measurement reflects the true whole-device draw.
-
-**Important measurement scope.** The Mojo is spliced on the main power rail (LiPo → Notecarrier CX `VBAT`), so it measures the **whole-device subsystem**: Notecard for Skylo, Notecarrier CX onboard regulators and solar charger, and the LSM6DSOX IMU, not the Notecard alone. The published Notecard idle figure (~8–18 µA @ 5V) applies to the Notecard's own power domain in isolation; the Notecarrier CX adds its own quiescent current from the onboard power management IC and charger. Use the Mojo figures below as a realistic whole-system deployment budget, not for direct datasheet comparison. To isolate the Notecard's draw, you would need to separate its VMODEM rail from the carrier board. See the [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for rail-isolation guidance.
-
-Expected current profile at default settings (30-second sample interval; 1 daily outbound + 3 inbound sessions):
-
-| Phase | Expected current (whole-device rail) |
-|---|---|
-| Host off between samples (Notecard + carrier idle) | ~20–60 µA @ 5V (Notecard idle + carrier quiescent) |
-| Host awake + accelerometer sampling (~2–3 seconds) | ~5–15 mA |
-| GNSS fix attempt (up to every 15 minutes, 10–60 seconds duration) | ~20–50 mA |
-| Notecard cellular session (LTE-M, small payload) | ~100–300 mA average, brief bursts |
-| Notecard NTN session (satellite, small payload) | ~150–400 mA average during transmission window |
-
-A healthy Mojo trace at default settings will show the following pattern:
-
-- **Flat near-zero baseline (~20–60 µA) between wakes** — Notecard idle plus Notecarrier CX quiescent draw while the Cygnet is fully powered off.
-- **Brief ~5–15 mA blip every 30 seconds, ~2–3 seconds long** — the Cygnet powering up, running the IMU sample burst, and classifying vibration before calling `goToSleep()`.
-- **~20–50 mA pulses at up to 15-minute intervals, 10–60 seconds long** — the Notecard GNSS module acquiring a location fix (periodic mode; actual cadence may be lower when the device is stationary).
-- **Three scheduled inbound sessions per day** — every 8 hours the Notecard briefly contacts Notehub to pull environment-variable updates (`inbound: 480`); each session is typically 10–30 seconds on LTE-M. Plus **one daily outbound session** carrying the queued `equip_summary.qo` (typically 10–60 seconds on LTE-M). On NTN, both inbound and outbound sessions may take longer depending on satellite availability.
-- **Additional radio sessions for each state-change event** — after each `equip_event.qo` Note is accepted by the Notecard, the firmware issues a `hub.sync` request to trigger an immediate Notecard sync outside the scheduled cadence. A typical active work day produces 2–6 state-change events (engine start, stop, possible midday transport legs), so expect 2–6 additional sessions above the baseline. Combined, expect roughly **6–10 total sessions per active work day** at default settings.
-- **Geofence-triggered transmissions** (if `geofence_radius_m` is set) — the Notecard emits a `_track.qo` autonomously on geofence exit and syncs it immediately.
-
-If the baseline is continuously 10+ mA, the Cygnet is not sleeping; confirm that the `ATTN → EN` jumper described in §5 is physically present and seated, then check that `NotePayloadSaveAndSleep` is not returning early (the serial output will confirm). If a cellular or NTN session is unusually long (>60 seconds on LTE-M), the radio is struggling with signal quality; check the MAIN antenna placement, verify it has an unobstructed sky view, and confirm the antenna is the Skylo-certified unit that ships with the NOTE-NBGLWX.
-
-**Solar viability estimate.** Because the Mojo is on the load rail, it measures consumption only; current flowing through the Notecarrier CX's separate solar charger input is invisible to it. With the solar panel disconnected and the unit running from a known LiPo or bench supply, run the Mojo for a full 24-hour period and Note total mAh consumed. Compare that figure against the theoretical harvest for your panel size and site: a 1W panel with 4 effective sun-hours produces **4 Wh (4000 mWh) raw**; after typical derating for panel temperature and incidence angle (~80%), charger conversion efficiency (~85%), and soiling (~90%), usable harvest is roughly **2.4 Wh — approximately 480 mAh at 5V**. At default settings the GPS cadence alone adds ~15–30 mAh/day; combined with host wakes, three daily inbound check-ins, one outbound sync, and typically 2–6 event sessions on active days, expect whole-device consumption of **65–120 mAh/day**. A 1W panel with ≥3 effective sun-hours per day typically covers this on cellular; satellite sessions draw more per session, so size the panel per-site. If Mojo shows a rising deficit across repeated 24-hour tests (solar disconnected), consider a 2–5 W panel, a higher-capacity LiPo, or a longer GPS period (`GPS_PERIOD_SECONDS`). To validate actual panel and charger harvest rather than relying on the derating model, place a DC current meter inline on the solar cable itself and log it over a representative sun-exposed day.
-
-## 10. Troubleshooting
-
-| Symptom | Likely Cause | Solution |
-|---------|--------------|----------|
-| Serial monitor shows no output after upload | Board not selected correctly, or USB driver missing | Verify **Generic STM32L4 → Cygnet** is selected in Arduino IDE; on Windows, install [ST-Link drivers](https://www.st.com/en/development-tools/stsw-link009.html). |
-| `[IMU] Not found` message | Accelerometer not powered or miswired | Check I2C connections (SDA, SCL, +3V3, GND) and verify LSM6DSOX is recognized by `Wire.scan()`. See [§5](#5-wiring-and-assembly). |
-| No `_session.qo` appearing in Notehub after several minutes | PRODUCT_UID mismatch, missing antenna, or no cellular/NTN coverage | Verify `PRODUCT_UID` matches the value you created in Notehub (copy-paste from Notehub → your project settings). Confirm MAIN antenna is mounted outside with unobstructed sky view; check the Notecard LED activity — it should flash briefly during sync attempts. If satellite, allow 5+ minutes for Skylo session establishment depending on sky visibility. |
-| Classifier always outputs `IDLE` even when tapping | Thresholds too high | Lower `vib_run_mg` to 10.0 (via Notehub Fleet → Environment); wait for next inbound sync (~8 hours) or manually trigger a device fetch. |
-| Classifier falsely triggers `RUNNING` during idle periods | Thresholds too low | Raise `vib_run_mg` to 20.0 and lower `vib_cv_max` to 0.30. |
-| `ATTN → EN` jumper error or baseline current 5–10 mA (not sleeping) | Jumper not installed or miswired | Verify the jumper physically connects `ATTN` and `EN` pins on the Notecarrier CX header (see [§5](#5-wiring-and-assembly), "ATTN → EN jumper" line). Measure with a multimeter to confirm continuity. Without it, the host runs continuously and the power budget collapses. |
-| Battery voltage in `equip_summary.qo` declining over days | Solar input inadequate | Check that the 6V solar panel is receiving adequate direct sunlight (≥3 effective sun-hours per day in temperate climates). If deployment is shaded or at high latitude in winter, upgrade to a 2–5 W panel. Use a DC meter inline on the solar cable to confirm actual harvest. |
-| Duplicate `equip_event.qo` Notes in Notehub | I2C acknowledgement lost after Notecard accepted Note | This is an edge case by design (at-least-once delivery). Downstream routes should dedup by `epoch` + `event` pair (the Note's timestamp and event type are unique per transition). |
-| Arduino-cli reports "board not found" or FQBN error | Core not installed or board name wrong | Run `arduino-cli core list` to confirm STMicroelectronics:stm32 is installed. If not, run the full `core install` command from [§7.1](#71-installing-and-flashing). Verify the FQBN is `STMicroelectronics:stm32:GenL4:pnum=CYGNET` (case-sensitive). |
-
-
-## 11. Limitations and Next Steps
-
-This is a reference build, not a finished fleet product. A few things are deliberately scoped down so the core idea (engine-hour billing from a magnetic stick-on with no wiring) can stay readable. The list below calls out where you'll want to harden the design before it goes on a thousand machines, then points at the natural production extensions.
-
-### Simplified for this POC
-
-Each of the following is a deliberate shortcut that keeps the reference build readable, with a note on what a production deployment would do instead.
-
-**The vibration classifier is heuristic, not trained.** The RMS + CV algorithm distinguishes engine idle from transport vibration well for diesel construction equipment at typical idle RPMs. On gasoline-powered equipment with smoother idle, CV can be lower and may overlap with transport characteristics at certain speeds, and on very rough terrain engine-running CV can creep above the default threshold. The `vib_run_mg` and `vib_cv_max` environment variables are the tuning knobs, but they **require per-equipment-class calibration from logged data** to dial in precisely. A production deployment would instrument a representative sample of each equipment type, log raw RMS/CV values over several shifts, and derive per-class threshold pairs.
-
-**Hour accumulation granularity is 30 seconds.** Each wake adds `SAMPLE_INTERVAL_SEC / 3600` hours to the running bucket if the previous state was RUNNING. A start event that occurs midway through a 30-second sleep interval will be captured on the *next* wake, so worst-case rounding error is one sample interval (30 seconds). For billing purposes this is typically acceptable; for sub-minute precision, reduce `SAMPLE_INTERVAL_SEC` to 10 at the cost of ~3× higher host wake frequency.
-
-**The first run session after power-on may report `session_min: 0`** if the Notecard has not yet acquired valid time. `getEpoch()` returns 0 when `card.time` has no cellular or GPS sync. The firmware guards against recording a start timestamp of 0; if the device transitions to RUNNING before time is valid, `run_session_start` is left at 0 and the closing `engine_stop` or `transport_start` event will carry `session_min: 0`, making it unusable as a standalone billing record. Subsequent sessions, once the Notecard has acquired time, compute correctly. For the affected cold-start window, the daily `equip_summary.qo` still accumulates run hours through the hour-meter buckets regardless of session boundaries; use the summary to reconcile any gap. Billing integrations should treat a `session_min: 0` on the first event after a device power-cycle as an **incomplete record**.
-
-**The persistent hour counter is software-based, not hardware-backed.** The `run_h_total` field in `PersistState` is persisted to Notecard flash on each sleep. If the Notecard is replaced or factory-reset, **the lifetime total is lost.** A production implementation should store the authoritative total server-side in Notehub (e.g., as a device-level environment variable updated on each `engine_stop` event) so it survives hardware replacement.
-
-**There is no geofence alerting in the application firmware.** Geofence exit is handled autonomously by the Notecard (`_track.qo`); the application firmware only configures the fence center via `card.location.mode`. The resulting `_track.qo` Notes contain location data but no application-level label. A production deployment should add a Notehub route that triggers an alert when `_track.qo` appears outside the fence window.
-
-**The geofence cannot be centered at the equator or prime meridian.** The firmware uses `geofence_lat = 0.0` and `geofence_lon = 0.0` as the "not configured" sentinel, and refuses to apply a fence if either coordinate is within ≈0.0001° of zero (the check is `fabsf(lat) > 0.0001f` and `fabsf(lon) > 0.0001f`). A deployment precisely on the equator (lat ≈ 0°) or the prime meridian (lon ≈ 0°) — for example, sites in southern Ghana, the Republic of Congo, or the English Channel — cannot use the geofence feature as currently implemented. To lift this restriction, replace the 0,0 sentinel with an explicit enable flag (e.g., a `geofence_enable` environment variable set to `1`) and allow lat/lon to take any in-range value including zero.
-
-**Solar panel sizing is for temperate climates.** A 1W panel + 2000 mAh LiPo provides adequate runtime in most regions with ≥3 effective sun-hours per day. At higher latitudes in winter, or when the enclosure is mounted on a shaded chassis location, a larger panel (2–5W) or a higher-capacity LiPo (5000 mAh) may be needed. The `bat_v` field in `equip_summary.qo` is the early-warning indicator; a steadily declining voltage over multiple days indicates harvest deficit.
-
-**Mojo is not read in firmware.** The firmware does not poll the Mojo's LTC2959 coulomb counter over Qwiic. Adding a `mojo_mah` field to `equip_summary.qo` is a straightforward extension if fleet-level energy telemetry is valuable to the operator.
-
-### Production Next Steps
-
-These are the natural extensions for hardening the tracker across a real fleet, from data-driven tuning through over-the-air updates and richer sensing.
-
-**Per-equipment-class threshold calibration** would deploy a "learning mode" firmware build that logs raw RMS/CV data at 1-minute intervals for several shifts before switching to production classification.
-
-**Lifetime hour counter persistence** in Notehub environment variables would let the running total survive device replacement.
-
-**A geofence-exit alert route in Notehub** would fire a webhook when a `_track.qo` Note appears with a location outside the configured fence, enabling unauthorized-move notifications.
-
-**Over-the-air firmware updates** via [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) to the Cygnet host would allow threshold algorithm improvements and new features without a technician visit to each machine.
-
-**Tamper detection** would distinguish an abrupt, very high-amplitude single-axis spike (e.g., the magnet base being removed) from equipment vibration and flag it as a `tamper` event.
-
-**Multi-sensor fusion** — pairing with an engine temperature sensor (NTC thermistor on the exhaust manifold) or a current clamp on the alternator output — would provide a second independent confirmation of engine state, improving classifier reliability on unusual equipment types.
-
-## 12. Summary
-
-The rental excavator that started this story now reports its own hours. A magnetic enclosure slaps onto the frame rail in under ten minutes (no drilling, no harness, no OEM cooperation) and from that moment the machine streams engine hours, location, and work-session events to Notehub over whichever network it can reach. The two-parameter RMS + coefficient-of-variation classifier is the piece that makes vibration-only detection trustworthy: it tells a diesel idle apart from a flatbed delivery without any wiring to the engine, and Notecard for Skylo keeps the data flowing whether the machine is at a regional yard or at the bottom of a quarry. For the rental company, the data pipeline is straightforward: any `equip_event.qo` with `session_min > 0` is the billing record, `run_h_total` drives maintenance scheduling, and a `transport_start` at 2 AM is the unauthorized-use alert. Same firmware, same hardware, same Notehub project, across the whole fleet.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/heavy-equipment-hours-of-use-utilization-tracker/)**
diff --git a/75-heavy-equipment-hours-of-use-utilization-tracker/banner.png b/75-heavy-equipment-hours-of-use-utilization-tracker/banner.png
deleted file mode 100644
index 4fbcea2a..00000000
Binary files a/75-heavy-equipment-hours-of-use-utilization-tracker/banner.png and /dev/null differ
diff --git a/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/01-system-architecture.svg b/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/01-system-architecture.svg
deleted file mode 100644
index 07f1e309..00000000
--- a/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/02-wiring-assembly.svg b/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 950e24a4..00000000
--- a/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,75 +0,0 @@
-
diff --git a/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/03-data-flow.svg b/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/03-data-flow.svg
deleted file mode 100644
index e3915174..00000000
--- a/75-heavy-equipment-hours-of-use-utilization-tracker/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
diff --git a/76-untethered-trailer-chassis-fleet-tracker/README.md b/76-untethered-trailer-chassis-fleet-tracker/README.md
index 68cd224d..12527550 100644
--- a/76-untethered-trailer-chassis-fleet-tracker/README.md
+++ b/76-untethered-trailer-chassis-fleet-tracker/README.md
@@ -1,600 +1,5 @@
-# Untethered Trailer & Chassis Fleet Tracker
+# Untethered Trailer and Chassis Fleet Tracker
-
+A solar-trickle-charged, tractor-independent GPS tracker for freight trailers and intermodal chassis, with cellular-first telemetry and Iridium LEO satellite fallback for global coverage.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Untethered%20Trailer%20%26%20Chassis%20Fleet%20Tracker).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/trailer_fleet_tracker_starnote/](firmware/trailer_fleet_tracker_starnote/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a solar-trickle-charged, tractor-independent GPS and motion tracker for [asset location tracking](https://blues.com/solutions-location-tracking/) on freight trailers and intermodal chassis. The device delivers cellular-first telemetry with truly global Iridium LEO satellite fallback, keeping the trailer or chassis visible across trans-oceanic container routes and polar corridors where geostationary satellite networks have no coverage at all. The hardware is a Blues Notecarrier XI with a Swan host, a cellular Notecard, and a Starnote for Iridium (see §4 for the BOM).
-
-The unit mounts on the trailer roof or chassis frame, runs a parked/moving state machine entirely on-device, and delivers departure, arrival, and position Notes to the [Blues Notehub](https://blues.com/notehub/) cloud service over cellular with automatic Iridium satellite fallback: no tractor hookup, no site IT, and no dependence on terrestrial coverage.
-
-## 1. Project Overview
-
-**The problem.** Long-haul trucking fleets have wrestled with a persistent visibility gap for decades: the tractor has an ELD (electronic logging device), a telematics unit, and a driver. The 53-foot box it drags around has none of those things. An intermodal chassis is even worse: the same steel frame might be owned by a chassis pool, leased to a carrier, loaded by a shipper, and dragged by three different tractors in a single week. At any given moment, a fleet operator's dispatch system knows where the power unit is. The trailer? It's wherever the last driver left it.
-
-This matters because trailers and chassis represent enormous capital. A $100,000+ refrigerated trailer that sits dark for five days at a shipper's dock is invisible to the fleet: the carrier can't bill for detention time it can't prove, can't recover equipment without calling around, and can't prevent the slow-drain of assets that leak out of rotation. At the chassis pool level, the problem is even more acute: intermodal equipment management is largely still a phone call business because there's no inexpensive, tractor-independent way to know where each piece of equipment is.
-
-**Why Notecard.** A trailer changes tractors every day and can change carriers every few days. Any solution that depends on the tractor — a J1939 tap, a cab-mount device, a driver's phone — fails the moment the trailer unhooks. The Blues Notecard is the right fit here for three reasons that compound:
-
-
-
-First, **cellular is tractor-independent**. The Notecard on the trailer roof has its own prepaid SIM and cellular session. It doesn't know or care what tractor pulls it.
-
-Second, **satellite fallback fills coverage gaps**, from remote land corridors to mid-ocean. North American freight routes cross the Great Plains, mountain passes, and border zones where LTE-M coverage has real gaps. Container chassis roam even further: trans-Atlantic and trans-Pacific segments put them completely beyond any terrestrial signal and any geostationary satellite footprint. This project addresses all of those scenarios: a standard cellular Notecard paired with a [Starnote for Iridium](https://dev.blues.io/datasheets/starnote-datasheet/starnote-for-iridium/) provides cellular-first delivery with Iridium LEO satellite fallback that works pole-to-pole, including every ocean segment. After two consecutive cellular failures the Notecard routes through the Starnote automatically; the firmware doesn't need to know which path is active.
-
-Third, **power is scarce**. Trailers have no reliable 12V auxiliary hookup; the 7-way connector only carries power when a tractor is connected, which is exactly the condition we don't need to rely on. A small solar panel and LiPo battery are the only viable power source, which means the entire system needs aggressive sleep discipline. The Swan enters deep sleep via the ATTN interrupt between checks. The firmware explicitly enables GPS on each PARKED→MOVING departure (issuing `card.location.mode {"mode":"periodic"}`) and disables it on each MOVING→PARKED arrival (issuing `card.location.mode {"mode":"off"}`), so the GPS module never runs during parked dwells. The result is a device that can run for days without solar input and maintain month-scale battery life on trickle solar.
-
-**Deployment scenario.** A weatherproof IP67 enclosure mounted flat on the trailer roof, with the solar panel installed on the same roof surface. Two SMA bulkhead fittings in the enclosure lid route the cellular antenna and the Iridium+GPS combined antenna to the lid surface; no GPS patch inside is needed. For chassis installations, see [Chassis deployment variant](#chassis-deployment-variant) for chassis-specific mounting guidance. No tractor hookup. No wiring into trailer or chassis existing systems.
-
-
-## 2. System Architecture
-
-
-
-### 2.1 Swan host responsibilities
-
-The brain of the tracker is the [Swan](https://dev.blues.io/datasheets/swan-datasheet/) STM32U5 host, mounted alongside the Notecarrier XI and wired to its 0.1" headers by hand because the XI has no Feather socket. On each wake the Swan asks the cellular Notecard's built-in accelerometer one question through [`card.motion`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-motion) — *moving or parked?* — detects a transition if one occurred, and queues the appropriate [Note](https://dev.blues.io/api-reference/glossary/#note) over I²C. PARKED→MOVING fires [`card.location.mode {"mode":"periodic"}`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location-mode) to start GPS; MOVING→PARKED fires [`card.location.mode {"mode":"off"}`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location-mode) to shut it back down for the dwell. Transition Notes are stamped at the wake where the change was first observed (current cached GPS fix and epoch) and that snapshot is stored in the pending-event queue immediately. Every delivery attempt, including retries on future wakes, reuses the stored snapshot, so a retried departure Note never picks up a stale location from a later parking period. With the queue handled, the Swan saves state to Notecard flash via `NotePayloadSaveAndSleep` and drops into deep sleep until the ATTN timer fires again.
-
-Departure Notes carry `sync:true`. On cellular that wakes the radio immediately for delivery. On NTN/satellite paths it marks the Note as high priority but can't interrupt an Iridium orbital pass on demand; the Notecard queues the Note and ships it at the next scheduled satellite transmission opportunity, typically minutes away depending on LEO geometry. Position and heartbeat Notes batch until the next outbound window on either transport.
-
-### 2.2 Notecard and Starnote for Iridium responsibilities
-
-The cellular Notecard (e.g., NOTE-WBEX) is where queued Notes live between syncs. It manages the cellular session on the cadence configured by [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) and, when two consecutive cellular attempts fail, routes through the [Starnote for Iridium](https://dev.blues.io/datasheets/starnote-datasheet/starnote-for-iridium/) module thanks to [`card.transport "method":"cell-ntn"`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport). The Starnote handles the Iridium LEO satellite session and GPS/GNSS through a single Iridium-certified combined antenna, and the standard [`card.location`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location) API returns the Starnote's fix transparently. The Notecard also distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from Notehub so the fleet can be retuned without a truck roll.
-
-### 2.3 Notehub responsibilities
-
-Once Notes leave the device, the Notecard's embedded global SIM carries them over supported carriers worldwide and delivers them to [Notehub](https://notehub.io). [Notehub](https://dev.blues.io/notehub/notehub-walkthrough/) ingests, stores, and routes events downstream from there. [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) let the operator group trailers by carrier, lane, or equipment type and apply shared threshold configurations, and [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) automatically assign trailers to the right fleet by region or device attribute.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations. Route setup is project-specific; see the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). This project ships no specific downstream endpoint.
-
-
-## 3. Technical Summary
-
-**What you'll have when you're done:** A solar-powered trailer or chassis tracker that reports motion-triggered departure/arrival events and position updates to Notehub over LTE-M (primary) or Iridium LEO satellite (automatic fallback). No tractor hookup, no site IT, global pole-to-pole coverage.
-
-1. **Notehub** — create a [Notehub project](https://notehub.io) and copy its ProductUID.
-2. **Wire the bench rig** — Notecarrier XI + Swan + cellular Notecard + Starnote for Iridium + solar chain; full pinout in [§5.1](#51-notecarrier-xi-swan).
-3. **Edit one line** of [`firmware/trailer_fleet_tracker_starnote/trailer_fleet_tracker_starnote_helpers.h`](firmware/trailer_fleet_tracker_starnote/trailer_fleet_tracker_starnote_helpers.h) — set `PRODUCT_UID` to your Notehub project's ProductUID (in Notehub: **Project Settings → ProductUID**).
-4. **Flash** via Arduino IDE or `arduino-cli` (see [§7.1](#71-dependencies-and-flashing)).
-5. **Watch** — open Notehub → **Events** tab. You should see a `_session.qo` within a few minutes of power-on — this confirms the first cellular session and clock sync. The firmware then queues a `trailer_heartbeat.qo` locally on the next parked-check wake (~5 minutes in). Because heartbeat Notes are **not** marked `sync:true`, they wait for the next scheduled outbound sync window rather than transmitting immediately: **~60 minutes** at a full battery (`voutbound high:60`) or **~120 minutes** at nominal charge (`voutbound normal:120`). Budget **1–2 hours from first power-on** before the first heartbeat appears in Notehub.
-
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "trailer_event.qo",
- "body": {
- "type": 1,
- "dwell_h": 18.5,
- "gps_valid": 1,
- "lat": 41.8781,
- "lon": -87.6298,
- "evt_time": 1746182400
- }
-}
-```
-
-## 4. Hardware Requirements
-
-
-
-**Hardware stack Note.** This design uses Notecarrier XI + Swan rather than the default Notecarrier CX/Cygnet stack because the ocean-capable Iridium path requires the dedicated Starnote connector exposed by the Notecarrier XI — the Notecarrier CX/Cygnet combination does not provide that interface and therefore cannot support Starnote for Iridium or Iridium NTN satellite fallback.
-
-
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| Notecarrier XI | 1 | Carrier board with M.2 Notecard slot, Starnote for Iridium connector, +VBAT screw terminal, Qwiic port, and 0.1" headers exposing I²C, ATTN, GPIO, and power rails for an external host MCU. **No Feather socket** — the Swan is wired to these headers by hand. |
-| Standard cellular Notecard, e.g., [NOTE-WBEX](https://dev.blues.io/datasheets/notecard-datasheet/note-wbex/) | 1 | Global LTE-M / NB-IoT cellular + GPS/GNSS in an M.2 module. The Notecard handles the cellular session and motion detection; the Starnote for Iridium provides satellite fallback and combined GPS+satellite antenna. |
-| [Starnote for Iridium](https://dev.blues.io/datasheets/starnote-datasheet/starnote-for-iridium/) | 1 | Iridium LEO satellite module with a single Iridium-certified antenna that handles both satellite and GPS/GNSS. Plugs into the Notecarrier XI Starnote connector. No additional GPS antenna required. |
-| [Swan (STM32U5)](https://dev.blues.io/datasheets/swan-datasheet/) | 1 | Blues Feather-format STM32U5 host MCU. The Notecarrier XI has no Feather socket, so the Swan is mounted alongside the carrier and wired by hand to the XI's 0.1" headers (3V3, GND, SDA, SCL, ATTN). Enters deep sleep via `NotePayloadSaveAndSleep` / `card.attn` ATTN interrupt between wake cycles. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(bench-only, not field-deployed)* | 1 | Coulomb counter for power validation during bench bring-up. Connects inline between LiPo and Notecarrier XI +VBAT pad via Qwiic; remove before field deployment. |
-| [SparkFun Sunny Buddy MPPT Solar Charger](https://www.sparkfun.com/sparkfun-sunny-buddy-mppt-solar-charger.html) | 1 | LT3652-based MPPT charger; accepts 6–20V solar input and charges a single-cell LiPo at up to 450 mA. MPPT ensures the panel operates at its maximum power point across the wide irradiance range seen on a trailer roof. |
-| [SparkFun Small Solar Panel — 0.6W, 6V ETFE](https://www.sparkfun.com/products/18725) | 1 | Flexible ETFE panel, weather-resistant, 6V nominal. At 4 peak sun hours it delivers roughly 250–300 mAh/day. Whether that exceeds the system's parked quiescent draw depends on the Starnote for Iridium board's unpublished standby current; measure the actual floor with Mojo (§10) before drawing final sizing conclusions. Mount flat on the trailer roof above the enclosure. |
-| [SparkFun Lithium Ion Battery — 2Ah (PRT-13855)](https://www.sparkfun.com/products/13855) | 1 | 3.7V / 2000 mAh cell with 2mm JST-PH connector. Provides multi-day backup through cloudy dwell periods. |
-| Cellular antenna *(included with NOTE-WBEX)* | 1 *(included)* | Ships in the NOTE-WBEX kit. Connects to the Notecard MAIN u.FL port via the u.FL-to-SMA pigtail and SMA bulkhead fitting below. Covers the LTE-M / NB-IoT frequency bands for the regions of deployment. Route through the first SMA bulkhead to the exterior of the enclosure lid. |
-| Iridium-certified combined Iridium+GPS antenna *(included with Starnote for Iridium)* | 1 *(included)* | Ships in the Starnote for Iridium kit. A single u.FL antenna that covers both Iridium satellite uplink (1616–1626.5 MHz) and GPS/GNSS — no separate GPS antenna is needed. **Must use only the included antenna**: [Starnote for Iridium is certified on Iridium's network exclusively with this antenna](https://dev.blues.io/datasheets/starnote-datasheet/starnote-for-iridium/); substituting another antenna voids Iridium network certification and risks network blocking. Route through the second SMA bulkhead to the exterior of the enclosure lid and mount with an unobstructed 360° sky view. |
-| u.FL to SMA pigtail, ~100 mm, e.g., [SparkFun Interface Cable SMA to U.FL, WRL-09145](https://www.sparkfun.com/products/9145) | 2 | Two pigtails: one for the NOTE-WBEX cellular MAIN u.FL port (routes to the cellular antenna via the first SMA bulkhead) and one for the Starnote's Iridium+GPS u.FL port (routes to the combined Iridium+GPS antenna via the second SMA bulkhead). |
-| SMA female panel-mount (bulkhead) connector, 50Ω | 2 | Two fittings in the enclosure lid: one for the cellular antenna (NOTE-WBEX MAIN path) and one for the Iridium+GPS combined antenna (Starnote path). Both antennas exit externally through the lid — no inside-mounted GPS patch is needed. The Amphenol RF 132289 is a standard panel-mount SMA female chassis connector (50Ω, through-hole); any electrically equivalent 50Ω SMA female panel-mount fitting with a compatible flange size is an acceptable substitute. |
-| IP67 weatherproof enclosure, ~130×80×50 mm | 1 | Protects the electronics from road spray, rain, and the wide temperature swings of a trailer roof installation. Because both antennas route through SMA bulkheads, **the lid material does not need to be RF-transparent** — ABS, polycarbonate, or aluminum are all acceptable. Mount flat on the trailer roof; a cable entry gland in the side wall admits the solar panel leads. |
-
-**Antenna Note.** The Starnote for Iridium ships with an Iridium-certified antenna that connects to its single u.FL port and covers both Iridium satellite link and GPS/GNSS — no separate GPS antenna is needed and the NOTE-WBEX GPS u.FL port is not used. The NOTE-WBEX ships with a cellular antenna for its MAIN u.FL port. Both antennas route through separate SMA bulkhead fittings to the exterior of the enclosure lid. Mount the Iridium+GPS antenna with an unobstructed 360° sky view — Iridium LEO satellites pass over every azimuth, so directional orientation does not matter.
-
-The cellular Notecard (e.g., NOTE-WBEX) ships with an active Blues SIM including 500 MB of data and 10 years of global cellular service: no monthly commitments, no activation fees. The Starnote for Iridium requires a separate Iridium satellite service plan and is not covered by the cellular SIM; contact [Blues](https://blues.com/contact-sales/) for current Iridium service details and activation.
-
-
-## 5. Wiring and Assembly
-
-
-
-### 5.1 Notecarrier XI + Swan
-
-The cellular Notecard seats in the M.2 slot and the Starnote for Iridium plugs into the XI's dedicated Starnote connector. The Notecarrier XI has no Feather socket, so the Swan is mounted alongside the carrier and wired by hand to the XI's 0.1" headers. Power, Qwiic, and both antennas land on accessible headers and u.FL ports.
-
-**Notecarrier XI interface reference:**
-
-| Interface | Location | Notes |
-|---|---|---|
-| **+VBAT** | Screw-terminal block, carrier edge | 3.4–4.2V single-cell LiPo input. Red wire to `+VBAT`, black wire to adjacent **GND**. During bench bring-up the Mojo `LOAD` output connects here via its JST-PH pigtail. |
-| **GND** | Screw-terminal block, adjacent to +VBAT | Shared power and signal return for battery, solar charger, and Qwiic. |
-| **Host MCU headers** | 0.1" header rows along the carrier edge | The XI has no Feather socket. Mount the Swan alongside the carrier and wire it by hand to these headers: Swan `3V3` ↔ XI `3V3`, Swan `GND` ↔ XI `GND`, Swan `SDA` (`PB9`) ↔ XI `SDA`, Swan `SCL` (`PB8`) ↔ XI `SCL`, and the XI `ATTN` pin to a Swan GPIO. |
-| **M.2 slot** | M.2 Key-E connector, carrier center | Cellular Notecard (e.g., NOTE-WBEX) seats at a 45° tilt, then pivots flat and is secured with the M.2 retaining screw. The M.2 key prevents reverse insertion. |
-| **Starnote connector** | Dedicated multi-pin connector, carrier face | Starnote for Iridium plugs straight down into this connector; it carries power, I²C, and the Iridium+GPS antenna passthrough. Align the Starnote module over the connector and press firmly until fully seated — no screws required. |
-| **Qwiic** | JST-SH 1mm 4-pin, carrier face | Mojo connects here via Qwiic cable for I²C mAh reporting during bench bring-up. The XI has onboard I²C pull-ups; no additional resistors required. |
-| **Swan USB-C** | USB-C port on the Swan module itself | Programs the Swan host and provides a CDC serial debug port at 115200 baud. Connect to this port (on the Swan, not any carrier-edge port) for programming and serial monitor. |
-
-**Power chain — bench bring-up (Mojo inline):**
-
-The Mojo sits inline between the LiPo and the Notecarrier XI +VBAT screw terminal, measuring device-side discharge current only.
-
-1. Solar panel → **Sunny Buddy** SOLAR IN terminal (6–20V).
-2. **LiPo battery** → **Sunny Buddy** LIPO terminal (JST-PH 2mm 2-pin; charging connection).
-3. **LiPo battery** → **Mojo** `BAT` input (JST-PH Y-adapter from the battery pigtail; same node as the Sunny Buddy LIPO connection).
-4. **Mojo** `LOAD` output → Notecarrier XI **+VBAT** screw terminal (red to `+VBAT`, black to `GND`).
-5. **Mojo** Qwiic → Notecarrier XI **Qwiic** port.
-
-**Power chain — field deployment (Mojo removed):**
-
-After bench bring-up is complete (see [§9](#9-validation-and-testing)), remove the Mojo before sealing the enclosure:
-
-1. Solar panel → **Sunny Buddy** SOLAR IN (unchanged).
-2. **LiPo battery** → **Sunny Buddy** LIPO terminal (unchanged).
-3. **LiPo battery** → Notecarrier XI **+VBAT** screw terminal directly.
-
-
-
-**Polarity Note.** Verify red-to-positive on every JST-PH connection before applying power. The Notecarrier XI +VBAT accepts 3.4–4.2V; exceeding 4.3V risks damage.
-
-
-
-**Antenna routing:**
-
-Two external antennas, requiring two SMA female bulkhead fittings in the enclosure lid:
-
-- **NOTE-WBEX cellular antenna (MAIN u.FL):** Route via a ~100 mm u.FL-to-SMA pigtail to one SMA female bulkhead fitting on the enclosure lid; thread the included cellular antenna onto the bulkhead from outside.
-- **Starnote Iridium+GPS combined antenna (Starnote u.FL):** Connect to the Starnote's u.FL port via a second ~100 mm u.FL-to-SMA pigtail and route to the second SMA female bulkhead fitting on the enclosure lid. Mount this antenna on the lid surface facing skyward with a clear 360° sky view — Iridium LEO satellites pass over every azimuth, so directional orientation does not matter. **No separate GPS antenna is needed**; the Starnote's combined antenna handles GPS, and the NOTE-WBEX GPS u.FL port is not used.
-
-The two SMA bulkhead fittings should be spaced at least 25 mm apart to minimize coupling between the two antenna paths. If any trailer structure overhangs the planned mount location, shift the enclosure fore or aft to clear it — partial sky obstruction above roughly 8° elevation will degrade Iridium link margin and GPS acquisition.
-
-**I²C / debug:**
-
-The Swan communicates with the cellular Notecard over the Notecarrier XI's onboard I²C bus. The Starnote for Iridium is managed by the Notecard via the Starnote connector — no additional host wiring is required for satellite or GPS operation. During development, connect the **Swan's USB-C port** (on the Swan module itself) to a host PC; it enumerates as a CDC serial port at 115200 baud.
-
-
-### Chassis deployment variant
-
-An intermodal chassis has no roof — it is a flat, low steel frame sized for a 20- or 40-foot ISO container. The BOM is identical to the trailer installation, but the mounting, solar, and antenna strategy must adapt to the chassis geometry.
-
-**Mounting location.** Mount the enclosure on the **front bolster or gooseneck cross-member** at the kingpin end of the chassis. This area sits directly under the container floor plate and is partially protected from horizontal impacts during container handling. Use stainless M6 hardware with split lock washers through pre-drilled holes; thread-locking compound (e.g. Loctite 243) on every fastener is recommended given the constant road vibration. Avoid the rear cross-members; they are more exposed to road spray from the axle group and to forklift contact near the rear twist locks.
-
-**Solar panel.** When a container is loaded, the chassis roof surface is blocked entirely. Mount the solar panel on a **bracket welded or bolted to the inboard face of the front bolster rail**, angled 30–45° skyward and oriented south in the northern hemisphere. Verify the bracket geometry clears the container twist-lock castings and does not protrude beyond the chassis rail envelope where it would interfere with loading or transport width limits. Budget conservatively: assume 2–3 peak sun hours per day for a chassis-mounted panel (compared with 4 on an unobstructed trailer roof), and consider upgrading to a 4 Ah LiPo for high-latitude or winter deployments.
-
-**Antennas.** Both the cellular antenna and the Iridium+GPS combined antenna need a clear sky view. Install both SMA bulkhead fittings on the **top or outboard face** of the enclosure lid and orient the enclosure so that face points upward or outboard, not buried against the chassis rail. On a loaded chassis, antennas mounted inside the container envelope will have the container floor plate directly overhead; position the enclosure forward enough that the antennas clear the front edge of the container.
-
-**Enclosure protection.** Chassis environments are harder than trailer roofs: road spray from below the frame is continuous, forklift tines pass close to the frame rails, and container-handling operations subject nearby hardware to impact and scuff loads. Use an IP67 enclosure with stainless fasteners throughout, protect cable entry glands with reinforced strain-relief fittings rated for abrasion and UV, and consider adding a steel guard plate over the enclosure if the mount location is within reach of forklift traffic.
-
-
-## 6. Notehub Setup
-
-### Creating the project and provisioning the device
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a new project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) — it looks like `com.your-company.your-name:trailer-tracker`.
-2. **Set the ProductUID in firmware.** Open [`firmware/trailer_fleet_tracker_starnote/trailer_fleet_tracker_starnote_helpers.h`](firmware/trailer_fleet_tracker_starnote/trailer_fleet_tracker_starnote_helpers.h) and replace the empty string on the `#define PRODUCT_UID ""` line with your value.
-3. **Power the unit.** On first cellular session the Notecard associates itself with your project automatically; no manual claim step required. The device appears in your project's **Devices** tab within a minute or two.
-
-### Fleet organization
-
-Create one [Fleet](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) per equipment category, for example, `reefer-trailers`, `dry-vans`, and `flatbed-trailers`. Fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) flow to every device in that fleet on the next inbound sync, so you can tune heartbeat cadence or motion sensitivity for the whole category at once. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can auto-classify trailers by device attribute if your fleet uses a consistent naming scheme.
-
-### Environment variables
-
-Navigate to **Fleet → Environment** (or **Device → Environment** for per-unit overrides) and add any of the variables below. Changes propagate in two steps: Notehub delivers the updated values to the Notecard on the next **inbound sync window** (configured at 2 hours at high battery, 4 hours at normal battery via the `vinbound` profile), and then the host firmware picks them up on its next **hourly env poll** (`ENV_POLL_SECS = 3600`). Under normal battery conditions, expect new values to be active within roughly **3–5 hours** of saving them in Notehub — no reflash, no truck roll.
-
-| Variable | Default | Purpose |
-|---|---|---|
-| `parked_check_mins` | `5` | How often the host wakes to poll the accelerometer while parked. Lower values detect departures sooner (transition detection accuracy is bounded by this interval) but increase average sleep current. Range: 1–60. |
-| `moving_ping_mins` | `15` | GPS position report interval while moving. Arrival detection accuracy is bounded by this interval. When changed, the firmware also reissues `card.location.mode` so the Notecard's GNSS fix cadence stays matched to the new report interval. Range: 5–60. |
-| `heartbeat_hours` | `6` | Alive-ping interval while parked. Set higher (e.g. `12`) for long-dwell equipment at known yards; lower (e.g. `2`) for high-value assets or demurrage monitoring. Range: 1–24. |
-
-### Routing
-
-Add two routes in Notehub:
-
-- **`trailer_event.qo`** → real-time delivery to a downstream HTTP endpoint, MQTT broker, or cloud function. These departure/arrival events are the data that fuel detention billing, recovery workflows, and missed-connection alerts.
-- **`trailer_location.qo` + `trailer_heartbeat.qo`** → long-term store (e.g. Snowflake, AWS S3, or a time-series database) for dwell-time analytics, lane utilization, and battery trending.
-
-Separating the two routes keeps alert latency independent of bulk-load throughput. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for supported destination types.
-
-### What you'll see in Notehub
-
-The **Events** tab in your project shows all Note types:
-
-- **`_session.qo`** — Notecard housekeeping on each cellular or satellite session; confirms connectivity.
-- **`trailer_event.qo`** — fires on every departure and arrival, delivered with `sync:true`. The body looks like:
-
- ```json
- {
- "type": 1,
- "dwell_h": 18.5,
- "gps_valid": 1,
- "lat": 41.8781,
- "lon": -87.6298,
- "evt_time": 1746182400
- }
- ```
-
- `type` 1 = departed, 2 = arrived. `dwell_h` is the number of hours the trailer sat parked before this departure; it will be 0 for arrival Notes. `gps_valid` is 1 when a valid GPS fix was available at detection time, 0 when no fix existed (e.g., on first departure from a freshly installed unit, ignore `lat`/`lon` when `gps_valid` is 0). `lat`, `lon`, and `evt_time` are the GPS coordinates and Unix epoch captured at transition detection time — the wake cycle on which the state change was first observed. Timestamp accuracy is bounded by `parked_check_mins` for departures and `moving_ping_mins` for arrivals; location is the Notecard's most recent cached fix at detection time. These values are stored in the event queue at detection time and preserved across retried deliveries so a Note retried on a later wake always carries the original detection-time data, not the post-transition GPS state.
-
-- **`trailer_location.qo`** — queued every `moving_ping_mins` while rolling; the compact envelope carries the GPS fix embedded by the Notecard. Visible on a map in Notehub's device view.
-- **`trailer_heartbeat.qo`** — fired every `heartbeat_hours` while parked. Body:
-
- ```json
- {
- "volt": 3.84
- }
- ```
-
- A downward trend in `volt` across consecutive heartbeats is an early warning that the solar panel or charge path needs attention.
-
-
-## 7. Firmware Design
-
-The firmware lives in [`firmware/trailer_fleet_tracker_starnote/`](firmware/trailer_fleet_tracker_starnote/). All application logic runs in `setup()`; `loop()` forces a system reset if ever reached, ensuring the tracker resumes normal operation.
-
-### 7.1 Dependencies and flashing
-
-**Install the `Blues Wireless Notecard` library:**
-
-Install [`note-arduino`](https://github.com/blues/note-arduino) via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`. See the [note-arduino releases page](https://github.com/blues/note-arduino/releases) for available versions.
-
-**Add the STM32 boards index:**
-
-Add `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**, then install "STM32 MCU based boards" via the Boards Manager. Select **Generic STM32U5 series → Swan** as the board.
-
-**Flashing — Arduino IDE:** open `trailer_fleet_tracker_starnote.ino`, select the Swan board, and click **Upload**. Connect to the **Swan's USB-C port** (on the Swan module itself).
-
-**Flashing — `arduino-cli`:**
-
-```bash
-# Find the FQBN for the Swan variant on your installed core
-arduino-cli board listall | grep -i swan
-
-# Compile and upload (replace FQBN and port with what listall reports)
-arduino-cli compile -b STMicroelectronics:stm32:GenU5:pnum=SWAN_R5 firmware/trailer_fleet_tracker_starnote/
-arduino-cli upload -b STMicroelectronics:stm32:GenU5:pnum=SWAN_R5 \
- -p /dev/cu.usbmodem* firmware/trailer_fleet_tracker_starnote/
-```
-
-**Debug serial logging:** The `#define usbSerial Serial` line in `trailer_fleet_tracker_starnote_helpers.h` is commented out by default. To enable logging during development, uncomment that line, recompile, and open the serial monitor at **115200 baud** on the Swan's USB-C port. You'll see `[heartbeat]`, `[departed]`, `[location]`, and `[sleep]` log lines on each wake cycle. Comment it back out before deploying to the field.
-
-### 7.2 Module responsibilities
-
-| Responsibility | Where in sketch |
-|---|---|
-| Notecard config: `hub.set`, `card.transport`, `card.location.mode`, `card.motion.mode` | `notecardConfigure()` |
-| Compact Note templates (all three Notefiles) | `defineTemplates()` |
-| Env-var overrides from Notehub | `fetchEnvOverrides()` |
-| Motion state query | `isMoving()` |
-| Time and voltage reads | `getEpoch()`, `getBatteryVoltage()` |
-| GNSS state capture for transition events | `captureGnssState()` |
-| GPS validity gate (location and heartbeat Notes) | `hasValidGnssFix()` |
-| State machine, sleep/wake scheduling | `setup()` |
-| Note emission | `sendTransitionEvent()`, `sendLocationNote()`, `sendHeartbeatNote()` |
-| Transition event FIFO queue and retry | `enqueuePendingEvent()`, `drainPendingQueue()` |
-| State persistence across sleep | `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### 7.3 Motion and GPS strategy
-
-Motion detection is handled by the Notecard's built-in accelerometer, configured with [`card.motion.mode`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-motion-mode). The firmware uses `motion:5, seconds:60`, meaning the Notecard declares the trailer "moving" when five or more motion events accumulate in a single 60-second bucket, and "stopped" when the bucket falls quiet. `sensitivity:2` (25 Hz / ±4G) is tuned to catch the low-frequency road vibration of a loaded trailer without triggering on wind buffeting or dock impacts while parked.
-
-GPS mode is managed explicitly by the state machine rather than relying on the Notecard's implicit periodic-mode motion-gating, which is documented only for the Notecard's own GPS module and not guaranteed for the Starnote for Iridium's combined GPS hardware path. On first boot, `notecardConfigure()` issues [`card.location.mode {"mode":"off"}`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location-mode) because the unit always enters parked state. On each PARKED→MOVING departure the firmware issues `card.location.mode {"mode":"periodic", "seconds":}` to start GPS acquisition; on each MOVING→PARKED arrival it issues `card.location.mode {"mode":"off"}` to shut the GPS module down for the parked dwell. This explicit toggling guarantees the GPS module never runs while the trailer sits parked, regardless of how the Starnote for Iridium's combined GPS hardware interacts with the Notecard on this path. On this hardware GPS is provided by the Starnote for Iridium's combined Iridium+GPS antenna; the standard `card.location` API returns the fix transparently.
-
-If the `moving_ping_mins` environment variable is later changed in Notehub, `fetchEnvOverrides()` stores the new `moving_ping_secs` value immediately. If the trailer is currently MOVING it also re-issues `card.location.mode {"mode":"periodic"}` with the updated `seconds` field right away; if parked, the new period is applied automatically on the next PARKED→MOVING departure. The most recent fix is embedded into location and heartbeat Notes via the compact template's `_lat` / `_lon` keywords; transition event Notes use explicit `lat`/`lon`/`evt_time` fields captured at transition detection time (see [§7.4](#74-event-payload-design)).
-
-**GPS fix validity gating.** Before queuing any Note that embeds location data, the firmware calls `hasValidGnssFix()`, which issues `card.location` and checks whether the Notecard reports a non-zero lat/lon with no error. `card.location` returns the last cached fix regardless of the current GPS mode (periodic or off); no additional GPS-on time is incurred by this check. This prevents freshly installed units — where GPS has never acquired a fix — from emitting Notes with silently-zeroed coordinates.
-
-- **`trailer_location.qo`** — suppressed entirely when no valid fix is available. A location Note with zeroed coordinates has no fleet value, and because `last_location_at` is not advanced on suppression, the firmware will retry on the next moving-state wake once a fix is acquired.
-- **`trailer_event.qo`** and **`trailer_heartbeat.qo`** — always sent (departure/arrival events and battery voltage are too important to suppress), but carry a `gps_valid` field (`1` = confirmed fix, `0` = no fix available). Downstream receivers can use this flag to distinguish a confirmed location from an invalid placeholder and suppress map plotting or geofence checks accordingly.
-
-**GPS fix capture on transition events.** On the wake cycle where a PARKED→MOVING or MOVING→PARKED transition is first detected, the firmware calls `captureGnssState()` — a single `card.location` query that returns the Notecard's currently cached lat/lon. `card.location` returns the last cached fix regardless of the current GPS mode; no additional GPS-on time is incurred. The captured coordinates, validity flag, and current epoch are stored in the `PendingEvent` struct. Every delivery attempt for that event (including retries on future wakes after communication failures) uses the stored snapshot, not the Notecard's GPS state at retry time. This means a departure Note retried two hours later still carries the departure-detection-time location and timestamp, not the current parked position.
-
-For **arrival events**, `captureGnssState()` is called while GPS is still in periodic mode (the disable-GPS step comes immediately after), so the cached fix is current within one `moving_ping_mins` interval of the stop. For **departure events** after a long parked dwell, `captureGnssState()` is called before GPS is re-enabled, so the cached fix is from the trailer's last trip — potentially hours or days stale; `gps_valid` will still be `1` because the fix is structurally valid even if aged. If fresh departure coordinates are a hard requirement, the firmware can be extended to enable GPS, poll `card.location` until a new fix is available, and then call `captureGnssState`, at the cost of 30–90 seconds of additional GPS-on time and battery draw on each departure event.
-
-### 7.4 Event payload design
-
-All three Notefiles use [`"format":"compact"`](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) templates to minimize over-the-air size. Iridium SBD enforces a maximum payload size per message, and the compact format strips the JSON envelope and stores Notes as fixed-length binary records; at the field sizes used here (uint8, float16, float32, int32), each Note is approximately 14–20 bytes on the wire, well within the limit.
-
-**Payload Reference (for downstream integrators):**
-
-| Notefile | Trigger | Fields | Notes |
-|---|---|---|---|
-| `trailer_event.qo` | Departure or arrival | `type` (1=departed, 2=arrived), `dwell_h` (parked duration in hours), `gps_valid` (1=fix, 0=none), `lat`, `lon`, `evt_time` | `sync:true`; immediate cellular delivery. Iridium fallback on next window if cellular unavailable. |
-| `trailer_location.qo` | Moving state, interval elapsed | `lat`, `lon`, `gps_valid`, `volt` | Batched; sent at next outbound window (60–360 minutes depending on battery state). |
-| `trailer_heartbeat.qo` | Parked state, interval elapsed | `volt`, `gps_valid` | Batched; sent every 6 hours (default, overridable). Used to confirm solar charging (declining volt = charging failure). |
-
-Sample `trailer_event.qo` (departed, 18.5 hours of dwell, GPS fix confirmed at detection time):
-
-```json
-{
- "file": "trailer_event.qo",
- "body": {
- "type": 1,
- "dwell_h": 18.5,
- "gps_valid": 1,
- "lat": 41.8781,
- "lon": -87.6298,
- "evt_time": 1746182400
- },
- "sync": true
-}
-```
-
-`type` 1 = departed, 2 = arrived. `dwell_h` is hours parked before this departure; it is `0` for arrival Notes. `gps_valid` is `1` when a valid GPS fix was available at detection time, `0` when no fix existed (e.g., a freshly installed unit, ignore `lat`/`lon` when `gps_valid` is `0`). `lat` and `lon` are the GPS coordinates captured at transition detection time; `evt_time` is the Unix epoch at that same wake. Timestamp accuracy is bounded by `parked_check_mins` (for departures) or `moving_ping_mins` (for arrivals), and location is the Notecard's most recently cached fix at detection time. These fields are written explicitly by the host at detection time so they are preserved correctly across retried deliveries. Unlike `trailer_location.qo` and `trailer_heartbeat.qo` — which use the Notecard's auto-populated `_lat`/`_lon`/`_time` keywords — event Notes use explicit host-supplied fields so that a Note retried on a later wake never picks up a stale post-transition GPS state.
-
-Sample `trailer_heartbeat.qo` (parked, battery healthy, GPS fix available):
-
-```json
-{
- "file": "trailer_heartbeat.qo",
- "body": {
- "volt": 3.84,
- "gps_valid": 1
- }
-}
-```
-
-A downward trend in `volt` across consecutive heartbeats is an early warning that the solar panel or charge path needs attention. `gps_valid: 0` on early heartbeats from a new unit is normal — it clears once the Notecard acquires its first fix.
-
-`trailer_location.qo` carries no application body fields; the GPS fix is the only payload, embedded by the Notecard from its last acquired fix. Location Notes are suppressed entirely when no valid fix is available, so every `trailer_location.qo` that reaches Notehub carries real coordinates.
-
-### 7.5 Low-power strategy
-
-**Host power gate.** After every state-machine cycle, the firmware calls `NotePayloadSaveAndSleep`, which serializes the `AppState` struct to Notecard flash and issues [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to put the host to sleep. `card.attn` fires an ATTN interrupt that wakes Swan from deep sleep after the configured interval. The next hardware wake re-enters `setup()` from cold; `NotePayloadRetrieveAfterSleep` at the top of `setup()` rehydrates the struct.
-
-**GPS suppression.** The firmware issues `card.location.mode {"mode":"off"}` on each MOVING→PARKED arrival and `card.location.mode {"mode":"periodic"}` on each PARKED→MOVING departure; a parked trailer never runs the GPS module. Given trailers spend the majority of their lives at docks, yards, and distribution centers, this explicit GPS gating is the single biggest power saving in the system.
-
-**Voltage-variable sync.** `hub.set` is configured with `voutbound:"high:60;normal:120;low:360;dead:0"` — as the solar battery drains, the cellular outbound sync interval stretches from 1 hour to 6 hours, then suspends entirely if the battery is critically depleted. Transition events use `sync:true` to request priority delivery regardless of the outbound schedule; on cellular this wakes the radio immediately, while on NTN it queues the Note for the next satellite transmission opportunity rather than suspending delivery entirely.
-
-### 7.6 Retry and error handling
-
-- The first Notecard request in `notecardConfigure()` uses `sendRequestWithRetry(req, 5)` — a 5-second retry window that handles the known cold-boot I²C race where the Swan comes up before the Notecard is ready.
-- `isMoving()`, `getEpoch()`, and `getBatteryVoltage()` all guard on a NULL response and return safe default values rather than crashing the state machine.
-- `fetchEnvOverrides()` checks the `err` field on the Notehub response before trusting the body; if the Notecard hasn't yet established a session, the env response will contain an error and the firmware continues with its last known defaults.
-- The `AppState` restore logic treats a failed `NotePayloadGetSegment` (corrupt payload or schema change after a firmware update) as a first-boot, safely re-initializing all defaults rather than running with undefined state.
-- Integer env var inputs are clamped to their documented ranges before being applied to the state, so a typo in Notehub can't set the ping interval to 0 or 65535 minutes.
-
-### 7.7 Key code snippet 1: transport configuration
-
-After two consecutive cellular failures, the Notecard automatically routes through the Starnote for Iridium module. No firmware involvement required; a single API call configures the fallback.
-
-```cpp
-J *req = notecard.newRequest("card.transport");
-JAddStringToObject(req, "method", "cell-ntn");
-notecard.sendRequest(req);
-```
-
-### 7.8 Key code snippet 2: compact template definition
-
-All three Notefiles use compact format so Notes are small enough to transmit intact over satellite. `trailer_location.qo` and `trailer_heartbeat.qo` use the Notecard's auto-populated `_lat`/`_lon`/`_time` keywords — those Notes are always emitted in real time, so the Notecard's current GPS state is the correct value to embed. `trailer_event.qo` uses **explicit host-supplied `lat`/`lon`/`evt_time` fields** because transition events may be retried on future wakes; capturing and storing the values at detection time ensures retries always carry the original departure or arrival location rather than a post-transition GPS state.
-
-```cpp
-// trailer_event.qo — explicit lat/lon/evt_time, NOT auto-populated _lat/_lon/_time
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "trailer_event.qo");
-JAddNumberToObject(req, "port", 50);
-JAddStringToObject(req, "format", "compact");
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "type", 21); // TUINT8
-JAddNumberToObject(body, "dwell_h", 12.1); // TFLOAT16
-JAddNumberToObject(body, "gps_valid", 21); // TUINT8: 1=valid fix, 0=no fix
-JAddNumberToObject(body, "lat", 14.1); // TFLOAT32: captured at detection time
-JAddNumberToObject(body, "lon", 14.1); // TFLOAT32: captured at detection time
-JAddNumberToObject(body, "evt_time", 14); // TINT32: epoch captured at detection time
-notecard.sendRequest(req);
-```
-
-### 7.9 Key code snippet 3: voltage-variable sync with host power gate
-
-The `voutbound` string stretches the sync interval as the battery drains; `NotePayloadSaveAndSleep` puts the Swan to sleep between wakes.
-
-```cpp
-// hub.set with voltage-variable outbound cadence
-J *req = notecard.newRequest("hub.set");
-JAddStringToObject(req, "mode", "periodic");
-JAddStringToObject(req, "voutbound", "high:60;normal:120;low:360;dead:0");
-JAddStringToObject(req, "vinbound", "high:120;normal:240;low:720;dead:0");
-notecard.sendRequestWithRetry(req, 5);
-
-// ... later, at the end of each wake cycle:
-NotePayloadDesc out = {0, 0, 0};
-NotePayloadAddSegment(&out, kStateSegId, &state, sizeof(state));
-NotePayloadSaveAndSleep(&out, sleep_secs, NULL);
-```
-
-### 7.10 Key code snippet 4: motion-triggered state transition
-
-Every departure starts with a `card.motion` query. The dwell time is calculated from the stored `parked_since` epoch and attached to the Note so fleet managers can measure detention without any external tracking. `captureGnssState()` is called once on the wake where the transition is detected — its result is stored in the `PendingEvent` FIFO and passed through to every `note.add` attempt so retried events always carry the original detection-time location and timestamp. See [§7.3](#73-motion-and-gps-strategy) for the discussion of GPS freshness on departures after long parked dwells.
-
-```cpp
-J *rsp = notecard.requestAndResponse(notecard.newRequest("card.motion"));
-const char *mode = JGetString(rsp, "mode");
-bool moving = (mode && strcmp(mode, "moving") == 0);
-notecard.deleteResponse(rsp);
-
-if (prev == STATE_PARKED && moving) {
- float dwell_h = (state.parked_since > 0 && now > state.parked_since)
- ? (float)(now - state.parked_since) / 3600.0f : 0.0f;
-
- // Capture GNSS state at departure-detection time; stored in the event FIFO
- // so every delivery attempt (including retries) uses the detection-time
- // location rather than the Notecard's GPS state at retry time.
- float cap_lat = 0.0f, cap_lon = 0.0f;
- uint8_t cap_gps_valid = 0;
- captureGnssState(cap_lat, cap_lon, cap_gps_valid);
-
- state.current_state = STATE_MOVING;
- enqueuePendingEvent(state, EVENT_DEPARTED, dwell_h,
- (time_ok && now > 0) ? now : 0U,
- cap_lat, cap_lon, cap_gps_valid);
- drainPendingQueue(state); // attempt immediate delivery
-}
-```
-
-
-## 8. Data Flow
-
-
-
-**Collected.** On every wake: the Notecard accelerometer's moving/stopped status. When moving: GPS coordinates from the Notecard's periodic GPS module. When parked: LiPo battery voltage from the Notecard's ADC.
-
-**Transmitted.**
-
-| Notefile | Trigger | Cadence | Transport |
-|---|---|---|---|
-| `trailer_event.qo` | State transition (depart or arrive) | On event, `sync:true` | Cellular (immediate); NTN satellite (next transmission window) |
-| `trailer_location.qo` | While moving, interval elapsed | Every `moving_ping_mins` (default 15 minutes) | Batched, outbound window |
-| `trailer_heartbeat.qo` | While parked, interval elapsed | Every `heartbeat_hours` (default 6 hours) | Batched, outbound window |
-
-On **cellular**, queued Notes flush at the outbound window (60 minutes at high battery, 120 minutes at normal battery, stretching to 360 minutes at low battery, matching `VOUTBOUND_PROFILE`). Transition events bypass the queue via `sync:true`, waking the radio immediately for delivery.
-
-On **NTN/satellite**, `sync:true` marks transition events as high priority but the Notecard cannot interrupt a satellite orbital pass on demand. Events are queued and delivered at the next scheduled Iridium transmission opportunity rather than triggering an immediate radio wake. This is a [documented Notecard behavior](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set); expect delivery within the next satellite window (typically minutes, depending on Iridium LEO geometry) rather than sub-60-second cellular latency.
-
-**Routed.** All three Notefiles flow to Notehub. From there, configure routes to fan them to your fleet-visibility platform, TMS webhook, or analytics store. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub).
-
-**Triggers and alerts.** A `trailer_event.qo` with `type:1` (departed) is the signal to close a detention clock, log an outbound scan, or trigger a "tractor has hooked" notification. A `trailer_event.qo` with `type:2` (arrived) starts the detention clock. A `trailer_heartbeat.qo` with `volt` declining below ~3.6V over consecutive reads warrants a yard check on the solar charging path.
-
-
-## 9. Validation and Testing
-
-**Expected event cadence.** On first boot, `last_heartbeat_at` is zero, so the heartbeat condition fires on the first wake where the Notecard has a valid clock (`now > 0`). The Notecard syncs its clock during the initial cellular session, which completes within a few minutes of first power-on; the heartbeat is therefore queued locally on the following parked-check wake (~5 minutes in). Because heartbeat Notes are **not** marked `sync:true`, the Notecard does not transmit them immediately — they ride the next scheduled outbound sync window. At full battery that window opens after **60 minutes** (`voutbound high:60`); at nominal charge after **120 minutes** (`voutbound normal:120`). Expect the first `trailer_heartbeat.qo` to appear in Notehub **roughly 1–2 hours after first power-on** under typical battery conditions.
-
-**Commissioning state.** On first boot (or after a reflash that clears the persisted payload), the firmware seeds `parked_since` with the current Unix epoch from the Notecard's clock. For any unit that has already been running in the field and is then reflashed, the clock is synced and the first departure will carry an accurate dwell measured from the reflash moment. For a brand-new unit at the very first power-on, the Notecard must complete its first cellular session before it has a time reference (typically within a few minutes); if the clock is not yet available when setup first runs, `parked_since` is left at 0 and the first departure will report `dwell_h: 0`. To avoid this edge case on a new unit, **let the device sit powered and stationary until a `_session.qo` appears in Notehub** (confirming clock sync) before the trailer moves.
-
-A trailer that has already been running and is sitting on the yard with a charged battery should produce:
-
-- Zero `trailer_event.qo` Notes (no hookups)
-- Zero `trailer_location.qo` Notes (not moving)
-- One `trailer_heartbeat.qo` every six hours (four per day)
-- Periodic `_session.qo` events confirming cellular connectivity
-
-After a hookup, departure, run, and drop, you should see two `trailer_event.qo` events (one departed, one arrived) plus a series of `trailer_location.qo` events spaced 15 minutes apart for the duration of the trip.
-
-**Bench validation with Mojo.** The Blues [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) is a precision coulomb counter that sits inline between the LiPo and the Notecarrier XI +VBAT pad. It reports cumulative mAh to the Notecard over Qwiic at 1% accuracy. See [§5.1](#51-notecarrier-xi-swan) for the inline placement instructions.
-
-**Expected current draw** (from [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/), the NOTE-WBEX datasheet, and the Iridium 9603N module datasheet):
-
-| Phase | Expected draw |
-|---|---|
-| Notecard idle (between syncs, radio off) | ~8–18 µA @ 3.7V |
-| Iridium 9603N modem in Notecard-managed sleep (cellular available, NTN not active) | ~30 µA (per Iridium 9603N module datasheet) |
-| Starnote for Iridium board supporting circuitry quiescent (regulators, level shifters, supervisory MCU) | **Not published by Blues — measure with Mojo before sizing** |
-| Swan host deep sleep (ATTN interrupt) | ~2–10 µA |
-| Swan active (brief wake cycle, ~1–2 seconds) | ~5–25 mA |
-| GPS acquisition via Starnote antenna (first fix, ~30–90 seconds) | ~20–50 mA |
-| Cellular session (LTE-M data transfer, NOTE-WBEX) | ~200–300 mA avg, ≤2 A peak |
-| Iridium satellite session (Starnote SBD uplink, ~15–30 seconds) | ~300–500 mA avg, ≤1.5 A peak |
-
-In `cell-ntn` mode the Notecard keeps the Iridium modem in a Notecard-managed low-power sleep state whenever cellular is available; the Iridium 9603N modem's published sleep current is ~30 µA. Blues does not publish a board-level quiescent figure for the Starnote for Iridium's supporting circuitry (voltage regulators, level shifters, and supervisory MCU). **That board-level number is the dominant unknown in the parked-mode power budget for this design, and no authoritative daily energy total or solar-panel sizing conclusion can be stated until it is measured.** Use the Mojo bench procedure (§5.1) to measure the full system quiescent floor before selecting the final panel and battery. The published Iridium 9603N modem sleep figure (~30 µA) is a lower bound on the Starnote board's contribution; the true board-level draw will be higher once regulator and supervisory-circuit losses are included.
-
-**Important:** Iridium satellite sessions can draw up to **1.5 A peak** during the transmit burst. The 2 Ah LiPo must be capable of delivering this peak without voltage sagging below 3.4 V (the Notecard minimum). A standard 2 Ah lithium polymer cell with low internal resistance (≤150 mΩ) handles this comfortably; avoid heavily discharged cells (below ~3.6 V) before a first satellite session.
-
-For high-latitude (above 60°N), winter-deployment, or chassis-mounted scenarios where panel harvest drops to 2–3 peak-sun-hours per day, upgrade to a 3–5 W panel and a 4 Ah LiPo to maintain adequate reserve through multi-day cloudy periods.
-
-Three Mojo trace signatures to recognize:
-
-- **Healthy (parked, cellular available):** a near-continuous quiescent floor (the measured Starnote board draw, use this run to establish the value), a brief ~2 seconds blip every 5 minutes (motion check + Swan wake), and one 10–30 seconds cellular burst at ~200 mA every 2 hours at normal battery.
-- **Satellite fallback session:** a 15–30 seconds burst at ~300–500 mA with a short high peak — this is the Iridium SBD exchange. Expect this when cellular has failed and the Notecard routes through the Starnote.
-- **Stuck awake:** continuous current well above the quiescent floor means `NotePayloadSaveAndSleep` is not driving the Swan into deep sleep. Check that the Swan ATTN wiring on the Notecarrier XI is intact and watch serial output for `[sleep]` lines.
-
-**Functional test without a real trailer.** To verify the departure/arrival state machine without driving anywhere: shake or tap the Notecarrier gently to trigger accelerometer motion events. The Notecard accumulates events in 60-second buckets, but the host only wakes to query `card.motion` on the `parked_check_mins` cadence (default 5 minutes). After shaking, wait up to **`parked_check_mins` + 60 seconds + cellular sync time** (roughly 6–7 minutes at defaults) before expecting a `trailer_event.qo` with `type:1` in Notehub. To speed up bench testing, temporarily lower `parked_check_mins` to `1` via a Notehub environment variable — then the motion check fires within about 90 seconds of the shake.
-
-For the arrival event, Note the **asymmetric detection latency**: once the tracker transitions to STATE_MOVING, it sleeps for `moving_ping_secs` (default 15 minutes) between wakes. Departure detection is bounded by `parked_check_mins` (5 minutes default), while arrival detection is bounded by `moving_ping_mins` (15 minutes default). To see a `type:2` Note after letting the unit sit still, wait up to **`moving_ping_mins` + sync time** (roughly 16–17 minutes at defaults).
-
-
-## 10. Troubleshooting
-
-**Device won't stay asleep (stuck awake, draining battery fast):**
-- Check that the **ATTN wiring** on Notecarrier XI is intact (Swan ATTN line to Notecarrier pin).
-- Watch serial output with `usbSerial` enabled (uncomment in helpers.h). You should see `[sleep]` log every cycle.
-- If no `[sleep]` line appears, `NotePayloadSaveAndSleep` is not being called or the Swan is waking immediately from the ATTN interrupt.
-- Verify that `card.attn` is configured in `notecardConfigure()` to use the ATTN interrupt mode.
-
-**No departure/arrival events in Notehub:**
-- Confirm the accelerometer is working: check Notehub for `_session.qo` events (connection handshakes). If none appear, the Notecard isn't connecting.
-- Check that `PRODUCT_UID` is set correctly in `helpers.h` (Notehub: **Project Settings → ProductUID**).
-- Physically move the device to trigger a departure. **Departure detection is bounded by `parked_check_mins` (default 5 minutes)**; wait up to 6 minutes and check Notehub.
-- For **arrival** (the device must be in MOVING state), wait up to **`moving_ping_mins` + sync time** (default ~16 minutes) for the Note to appear.
-
-**Position always shows as GPS invalid (gps_valid=0):**
-- When parked, GPS is deliberately off (see §7.3). Trigger a departure so the device enters MOVING state and enables periodic GPS.
-- Once moving, GPS acquisition takes 30–90 seconds for first fix. Position Notes embed the most recent fix; early Notes in a trip may show `gps_valid=0` if no fix was acquired yet.
-- Check antenna routing: the Starnote's u.FL port must route through the SMA bulkhead fitting to the combined Iridium+GPS antenna on the enclosure exterior.
-
-**Low battery warnings (volt < 3.6V) persist:**
-- The 0.6 W solar panel provides ~250–300 mAh/day under good conditions (4 peak sun hours). In cloudy weather or high-latitude winter, this won't offset the ~30–50 µA quiescent draw.
-- Run a Mojo current-draw trace (§10) to confirm the measured quiescent floor. If it's significantly above the expected 30–60 µA range, a supporting regulator may be failing.
-- For harsh environments, upgrade to a **3–5 W panel** and a **4 Ah LiPo** (see §11).
-
-**Iridium fallback isn't triggering (device only sends on cellular):**
-- Iridium satellite mode activates automatically only when cellular is unavailable. On a bench with cellular available, events will always use LTE-M.
-- To test Iridium: move the device to a location with no cellular coverage (verified with a phone). On the next transmission window, the Notecard will route through the Starnote.
-- Check that the [Starnote firmware is up-to-date](/starnote/starnote-firmware-releases/).
-
-**Template registration fails (see error in serial log):**
-- The compact templates (port 50, 51, 52) are registered once on first boot. If registration fails, the device retries after 60 seconds (CONFIG_RETRY_SECS).
-- Confirm Notehub connectivity by checking for `_session.qo` events.
-- If the same ProductUID is used on multiple devices or in multiple projects, template port collisions can occur. Ensure each device uses a unique PRODUCT_UID.
-
-----
-
-## 11. Limitations and Next Steps
-
-A reference design has to draw the line somewhere, and this one draws it at the core problem: tractor-independent visibility with global coverage on trickle solar. The items below are the deliberate trade-offs and the places where a production deployment will add hardening. None of them are bugs, but they're all worth knowing before scaling past a pilot.
-
-### Simplified for this POC
-
-The following are the deliberate trade-offs in this build; each is something you should understand before scaling past a pilot.
-
-**Transition detection latency and GPS freshness are wake-bound.** The firmware samples motion only on wake boundaries — every `parked_check_mins` while parked (for departure detection) and every `moving_ping_mins` while moving (for arrival detection). Transition events are stamped with the wake time and the Notecard's cached GPS fix at that moment, not the exact physical instant of hookup or drop. For departure events after a long parked dwell, the GPS module has been off the entire time, so the cached fix may be from the trailer's last known pre-dwell location, **potentially hours or days stale**; `gps_valid` is still `1` because the fix is structurally valid, only its freshness is in question. Retried deliveries always carry the original detection-time capture (never re-stamped with the current state). If fresh departure coordinates are a hard requirement, the firmware can be extended to issue `card.location.mode {"mode":"on"}` and poll until a valid fix is available before enqueuing the departure event, at the cost of 30–90 seconds of additional GPS-on time per departure.
-
-**Satellite sync introduces latency.** Over Iridium NTN, `sync:true` event Notes are queued rather than transmitted immediately; the Notecard cannot interrupt a satellite orbital pass on demand the way it can wake a cellular modem. Departure and arrival events will be delivered at the next Iridium transmission opportunity; depending on LEO geometry, that window may be seconds to a few minutes away. This is a [documented Notecard behavior](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set).
-
-**The first-boot dwell baseline depends on clock sync.** On first boot (or after a reflash that invalidates the persisted payload), the firmware seeds `parked_since` with the current epoch if the Notecard's clock is available. For a brand-new unit, the Notecard needs to complete its first cellular session to sync its clock; if the trailer moves before that sync, the first departure reports `dwell_h: 0`. Allow the unit to reach a `_session.qo` in Notehub (confirming clock sync) before the first trip to ensure the first departure carries a meaningful dwell value.
-
-**There is no motion-event persistence check, so short moves are a blind spot.** The Notecard's 60-second / 5-event motion bucket (`card.motion.mode motion:5, seconds:60`) is the only debounce layer: the modem declares a window "moving" only when five or more accelerometer events accumulate within 60 seconds, which filters brief impulses from dock impacts or adjacent-equipment vibration. However, **the host firmware acts on the first parked-state wake where `card.motion` reports `moving` — there is no second-sample persistence check at the host level.** A single `moving` read immediately enqueues a departure event. The direct corollary: **a move that begins and ends entirely within one `parked_check_mins` interval is invisible to this firmware.** The trailer can depart, travel, and re-park between two consecutive host wakes and the host never observes a `moving` reading. At the default 5-minute parked-check cadence, short yard moves and brief tractor hookup attempts that resolve before the next wake may be silently missed. Production deployments with short-move visibility requirements should reduce `parked_check_mins` (e.g., to 1–2 minutes via the env var) and/or extend the firmware to require two consecutive `moving` reads before enqueuing a departure event. The `motion` and `seconds` parameters in `card.motion.mode` can also be tuned per equipment type to adjust bucket sensitivity.
-
-**Each position Note carries a single GPS fix.** Location Notes embed the Notecard's most recent periodic GPS fix. On a fast highway run, the fix embedded in any given Note may be up to `moving_ping_mins` old. Reducing `moving_ping_mins` to 5 minutes via the env var gives more frequent fixes at the cost of more Notes per trip.
-
-**Solar sizing is minimal.** The 0.6 W panel is sized for trickle charging a parked trailer in normal operating conditions. Extended cloudy weather, high-latitude winter deployments, or physically shaded mounting locations may not provide enough solar input to offset even the modest quiescent draw. For harsh environments, a 3–5 W panel and a larger LiPo (4 Ah or more) are more appropriate.
-
-**There is no tamper or cargo detection.** The scope here is location and dwell. Sensor additions for door-open detection, cargo weight, temperature, or tire pressure are natural extensions but not implemented in this POC.
-
-**Mojo is bench-only.** The firmware does not read Mojo's coulomb counter registers; it just flows through the Mojo's power path. Adding a cumulative mAh field to the heartbeat Note is a straightforward extension.
-
-**DFU is not wired up.** [Notecard Outboard Firmware Update](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) on the Swan is not configured in this POC. Field firmware updates currently require physical access to the Swan's USB-C port.
-
-**An alternative hardware path uses [Skylo](https://www.skylo.tech/) NTN for land routes only.** For fleets confined to North American land-route corridors within Skylo's geostationary footprint, [Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) on a [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) integrates cellular, Skylo NTN satellite, GPS, and the accelerometer in a single M.2 module with no Starnote or external MCU board required. The same Notefile schemas (`trailer_event.qo`, `trailer_location.qo`, `trailer_heartbeat.qo`) and the same Notehub project apply. **Skylo's service area covers defined land-route corridors only: no ocean-route or polar coverage.** See the [Choosing Between Skylo and Iridium](https://dev.blues.io/starnote/choosing-between-skylo-and-iridium/) guide for the full coverage comparison.
-
-### Production Next Steps
-
-Beyond the location-and-dwell core, these extensions integrate the tracker with trailer systems and back-office workflows.
-
-**J1939 / trailer ABS hookup** via the 7-way trailer connector would read brake and ABS status from the trailer's onboard systems when a tractor is connected.
-
-**A door sensor** (magnetic reed switch on the rear doors) would add load/unload event detection, mapped to a `trailer_cargo.qo` Notefile.
-
-**Detention billing automation** would use a Notehub route that fires a webhook into the TMS on every `trailer_event.qo` with `type:1` (departed), using the `dwell_h` field to auto-generate detention invoices for any dwell exceeding the contracted free-time allowance.
-
-**Outboard DFU** — wiring the Notecard's DFU GPIO to the Swan RESET/BOOT0 pins — would enable over-the-air host firmware updates across the entire fleet via Notehub.
-
-
-## 12. Summary
-
-The carrier whose $100,000 reefer was invisible at a shipper's dock now has eyes on it directly — no tractor required. A cellular Notecard plus Starnote for Iridium on a Notecarrier XI with a Swan host delivers cellular-first telemetry with pole-to-pole Iridium LEO satellite fallback, so the same hardware works on a regional dry-van running I-80 and on a chassis crossing the Pacific. GPS stays off during parked dwells, the Swan sleeps between motion checks, and voltage-variable sync stretches the cellular window as the battery drains, keeping the whole device living comfortably on trickle solar. Transition events are stamped at detection time and replayed faithfully across retries, so the dispatch view shows the moment a trailer departed rather than wherever it happened to be when the Note finally landed. The same architecture extends naturally to chassis pools, container yard management, and intermodal drayage: anywhere the visibility problem is "where is the non-powered equipment, regardless of who's pulling it today?"
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/untethered-trailer-and-chassis-fleet-tracker/)**
diff --git a/76-untethered-trailer-chassis-fleet-tracker/banner.png b/76-untethered-trailer-chassis-fleet-tracker/banner.png
deleted file mode 100644
index 06fd380e..00000000
Binary files a/76-untethered-trailer-chassis-fleet-tracker/banner.png and /dev/null differ
diff --git a/76-untethered-trailer-chassis-fleet-tracker/diagrams/01-system-architecture.svg b/76-untethered-trailer-chassis-fleet-tracker/diagrams/01-system-architecture.svg
deleted file mode 100644
index 3fae7dd3..00000000
--- a/76-untethered-trailer-chassis-fleet-tracker/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/76-untethered-trailer-chassis-fleet-tracker/diagrams/02-wiring-assembly.svg b/76-untethered-trailer-chassis-fleet-tracker/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 48868811..00000000
--- a/76-untethered-trailer-chassis-fleet-tracker/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,75 +0,0 @@
-
diff --git a/76-untethered-trailer-chassis-fleet-tracker/diagrams/03-data-flow.svg b/76-untethered-trailer-chassis-fleet-tracker/diagrams/03-data-flow.svg
deleted file mode 100644
index 5f31ed26..00000000
--- a/76-untethered-trailer-chassis-fleet-tracker/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
diff --git a/78-rail-car-condition-interchange-tracker/README.md b/78-rail-car-condition-interchange-tracker/README.md
index 73f58e27..a73502f9 100644
--- a/78-rail-car-condition-interchange-tracker/README.md
+++ b/78-rail-car-condition-interchange-tracker/README.md
@@ -1,595 +1,5 @@
# Rail Car Condition & Interchange Tracker
-
+A cellular and satellite reference design for leased freight and tank rail cars that monitors location, shock events, and coupler state, with optional lading pressure and temperature monitoring.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Rail%20Car%20Condition%20%26%20Interchange%20Tracker).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/rail_car_tracker/](firmware/rail_car_tracker/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [supply chain tracking](https://blues.com/solutions-supply-chain-tracking/) reference design for leased freight and tank rail cars. A solar-powered [Notecard for Skylo](https://shop.blues.com/products/notecard?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link), Blues' all-in-one cellular and satellite module, pairs with an embedded Cygnet STM32 host to monitor **car-level** conditions: location, shock/impact events, and coupler state. On TANK_CAR builds, the same device adds low-pressure vent-port fitting pressure via the Adafruit MPRLS (0–25 PSI absolute) and single-point lading temperature via a waterproof DS18B20 probe.
-
-## 1. Project Overview
-
-**The problem.** A leased tank car or intermodal flat leaves a chemical plant in Texas headed for a refinery in New Jersey. It crosses four railroads, rolls through a classification yard in Tennessee for three days, then moves overnight to a second yard before final delivery. The lessor has no idea where it is unless the lessee files an EDI interchange report, which may or may not be accurate, and which tells the lessor nothing about *condition*. Did the car take a hard coupling impact? Is a fitting pressure trending down, suggesting a valve leak? Was the car decoupled from its consist somewhere in Iowa at 2 AM? Without on-car telemetry, none of those questions have timely answers.
-
-Rail car lessors operate fleets worth hundreds of millions of dollars with essentially no real-time visibility below the interchange report level. That gap drives everything from underutilized assets (a car in a yard for two weeks that nobody can locate) to safety incidents discovered only after delivery.
-
-**Why Notecard for Skylo.** Rail corridors are the canonical example of connectivity infrastructure that doesn't follow population. For every mile of track through Chicago or Houston, there are fifty through Montana, the Appalachian plateau, or the Texas Panhandle where cellular coverage is thin to nonexistent. A device that relies on LTE alone will go dark for hours or days at a time in exactly the stretches where condition changes are most likely to go undetected. Rail cars have no access points to pair to; WiFi has no role in this deployment.
-
-
-
-Notecard for Skylo (NOTE-NBGLWX) solves the coverage gap with a single M.2 module that carries LTE-M/NB-IoT/GPRS for cellular coverage and [Skylo](https://www.skylo.tech/) NTN (Non-Terrestrial Network) satellite for everywhere else. The Notecard orchestrates the cellular-to-satellite fallback autonomously; the host firmware doesn't need to know which transport is in use. Notes accumulate in Notecard flash during the long dead zones between windows, then flush the moment any transport opens. For assets that might spend days out of cellular range and weeks in a yard, that queue-and-forward model is load-bearing, not a nice-to-have.
-
-This architecture maps directly to Blues' [supply chain tracking](https://blues.com/solutions-supply-chain-tracking/) use case: mobile assets crossing connectivity boundaries unpredictably, with no fixed infrastructure to rely on.
-
-**Deployment scenario.** A weatherproof NEMA 4X enclosure bolted to the car's end-sill or side-sill, powered by a small rooftop solar panel through a solar LiPo charge controller that safely charges a lithium-ion polymer battery. A [Blues Scoop](https://shop.blues.com/products/scoop?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) lithium-ion capacitor buffer sits inline between a 5 V regulated supply and the Notecarrier VBAT rail, smoothing out the high-current bursts that cellular and especially satellite transmission demand. Without it, a modest LiPo under a weak winter sun can brown out the radio mid-session. The Skylo-certified LTE/NTN antenna (included with Notecard for Skylo) and a passive GNSS antenna mount flush to the roof with a clear sky view. A magnetically actuated reed switch attaches near the coupler knuckle, with a matching magnet mounted to the adjacent coupler structure. On TANK_CAR builds, the MPRLS connects to a low-pressure vent or fitting port on the car body.
-
-## 2. System Architecture
-
-**Device-side responsibilities.** The work on the car itself is bounded by one constraint: a 15-minute wake window that has to do everything and then disappear. The Cygnet STM32L433 host on the Notecarrier CX comes up via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) sleep, reads its sensors, scores shock events, looks for coupler-state edges, evaluates three alert conditions on standard builds (seven on TANK_CAR builds), and queues Notes to the Notecard over I²C. The moment that's done it goes back to sleep, fully powered off, with the Notecard holding the persistent state struct in its own flash until the next ATTN fire rehydrates it.
-
-**Notecard responsibilities.** Everything that has to think about the network lives in the Notecard, not the host. It holds [Notes](https://dev.blues.io/api-reference/glossary/#note) in its on-device queue, runs GPS position fixes every five minutes while motion is detected (motion-gated so a car sitting in a yard isn't burning battery on GNSS), and syncs outbound data on a voltage-variable [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) schedule that stretches the interval as the battery drains. Transport selection is fully autonomous: if LTE-M can't reach a tower the Notecard switches to Skylo NTN and ships the queued Notes over satellite. The firmware never asks which path was used. The Notecard also distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from the [Blues Notehub](https://blues.com/notehub/) cloud service, so fleet-wide thresholds can be retuned without a truck roll.
-
-**Notehub responsibilities.** Once a Note leaves the car, the Notecard's embedded global SIM carries it over supported carriers worldwide and delivers it to [Notehub](https://notehub.io), which ingests events, stores them, and applies project-level [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). The three Notefiles (`railcar_status.qo`, `railcar_alert.qo`, `railcar_location.qo`) are deliberately separate so each can take its own downstream path: status Notes flow to a long-term analytics store for trend analysis; alert Notes fan out to an on-call endpoint (email, SMS, webhook, CMMS ticket) in near-real time; location Notes feed a geofencing service or time-series location store where interchange-boundary detection happens.
-
-**Routing (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, and Snowflake routes. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for setup; this project ships no specific downstream endpoint. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can organize cars by lessee, route, or car type (tank vs. flat) and route them differently at the fleet level.
-
-
-
-## 3. Technical Summary
-
-**What you'll have when you're done:** a weatherproof, solar-powered electronics sidecar that mounts to a freight car, samples sensors every 15 minutes, scores shock events, detects coupler state changes, and reports GPS location and condition through Notehub: automatically using cellular when available, switching to satellite in remote areas, and queuing everything offline until connectivity returns.
-
-**Fastest path to first event (bench rig, ~1 hour):**
-
-1. Create a [Notehub project](https://notehub.io), copy the **ProductUID** (shown under **Project Settings → ProductUID**).
-2. Set `PRODUCT_UID` in `firmware/rail_car_tracker/rail_car_tracker_helpers.h` (line 40: replace `""` with your ProductUID).
-3. Connect ADXL345 accelerometer and reed switch to [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) over I²C/D5 (see [§5 Wiring](#5-wiring-and-assembly) for pinout).
-4. Flash with `arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/rail_car_tracker/ && arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/rail_car_tracker/` (exact commands in [§7.1](#71-installing-and-flashing)).
-5. Open Notehub **Devices** tab — the Notecard appears within a few minutes. Within 15 minutes you'll see `railcar_status.qo` with `coupled`, `moving`, `shock_peak_g`, and `shock_windows` fields. See [§6 What you should see](#what-you-should-see-in-notehub) for sample JSON payloads.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "railcar_alert.qo",
- "body": {
- "alert": "impact",
- "value": 3.8,
- "_lat": 41.4993,
- "_lon": -81.6944,
- "_ltime": 1713888000
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with onboard Cygnet STM32L433 host — no separate MCU needed. I²C, six analog inputs, nine digital I/O, and M.2 Notecard slot. See the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/). |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Single M.2 module with LTE-M/NB-IoT/GPRS + Skylo NTN satellite. 500 MB cellular + 10 KB satellite bundled; no monthly fees. **Ships with its Skylo-certified LTE/NTN antenna — do not substitute another antenna** without a CTIA/OTA delta test report; Skylo may block uncertified devices. See [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/). |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter for bench power validation. See [§9](#9-validation-and-testing). Not deployed to the field. See the [Mojo datasheet](https://dev.blues.io/datasheets/mojo-datasheet/). |
-| [Blues Scoop](https://shop.blues.com/products/scoop?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Lithium-ion capacitor (250 F) peak-current buffer, wired **inline** between the 5 V boost module and the Notecarrier CX `+VBAT` rail. The Scoop has two connectors and two sets of header pins: **J1 (CHG)** JST input (4.8–24 V; headers **J3** are a through-hole alternative to J1) and **J2 (OUT)** JST output (2.5–3.8 V; headers **J4** are a through-hole alternative to J2). The 5 V boost module output connects to J1/J3; Scoop J2/J4 connects to Notecarrier CX `+VBAT`. The internal LiC charges from J1 between radio sessions and supplements the supply rail through J2 during high-current cellular and satellite bursts. **J1 requires ≥ 4.8 V — do not connect it directly to the LiPo (max 4.2 V) or to a raw solar panel.** It is **not** a LiPo charger and does not replace the solar charge controller. See the [Scoop datasheet](https://dev.blues.io/datasheets/scoop-datasheet/). |
-| 5 V boost regulator, 3.0–4.2 V input, 5 V regulated output, ≥ 500 mA (e.g., Pololu U1V11F5 or equivalent single-cell LiPo boost module) | 1 | Converts LiPo voltage (3.0–4.2 V) to the regulated 5 V that the Scoop J1 (CHG) input requires. Connect the LiPo JST to this module's input and the 5 V output to Scoop J1 (see [§5](#5-wiring-and-assembly)). A 500 mA output rating provides sufficient current to recharge the Scoop's internal LiC between radio sessions. |
-| [Adafruit Triple-Axis Accelerometer ADXL345 (product 1231)](https://www.adafruit.com/product/1231) | 1 | ±16 G, I²C, 3.3 V. Provides raw G-force readings for shock scoring at ±16 G full resolution. The Notecard's internal accelerometer tracks motion state but doesn't expose raw G values — the ADXL345 is needed for impact magnitude scoring. |
-| [Adafruit MPRLS Ported Pressure Sensor Breakout (product 3965)](https://www.adafruit.com/product/3965) | 1 | **TANK_CAR builds only** (enable by uncommenting `#define TANK_CAR` in `rail_car_tracker_helpers.h`). I²C, 3.3 V, **0–25 PSI absolute** (0–1723 hPa), ±0.25 % FSS. Returns absolute pressure — at sea level a port open to atmosphere reads approximately 14.7 PSI absolute. Suitable for monitoring fitting pressure on the car body where working pressure is within the 0–25 PSI absolute range. **Not rated for DOT-111, DOT-105, or higher-class tank pressure.** For production cargo pressure monitoring use a certified industrial transducer — see [§11](#11-limitations-and-next-steps). Omit from the BOM entirely for intermodal flat, boxcar, gondola, and other non-tank builds. |
-| [Adafruit DS18B20 Waterproof Digital Temperature Sensor (product 381)](https://www.adafruit.com/product/381) | 1 | **TANK_CAR builds only.** One-wire (OneWire protocol), 3.3–5 V, −55 to +125 °C, ±0.5 °C accuracy (−10 to +85 °C range). Configured at 12-bit resolution (0.0625 °C). Connects to pin `D6` on the Notecarrier CX header. Extend the probe through a sealed cable gland into the lading compartment for single-point cargo temperature monitoring. Verify chemical compatibility of the stainless-steel probe housing with the lading before installation. |
-| 4.7 kΩ resistor, ¼ W, through-hole or SMD 0603/0805 | 1 | **TANK_CAR builds only.** Pull-up resistor for the DS18B20 one-wire data line. Connects between the `D6` data line and the `+3V3_OUT` rail (see [§5](#5-wiring-and-assembly)). Standard 1 % or 5 % tolerance; any common supplier (Yageo, Bourns, Vishay, etc.). |
-| Magnetic reed switch, N.O. contacts, weatherproof (e.g. [Adafruit 375](https://www.adafruit.com/product/375)) | 1 | Mounts near the coupler knuckle. Closes when the paired actuator magnet is within range. Detects coupled/decoupled state. Adafruit 375 is sold individually; source a compatible cylindrical rare-earth magnet separately (see next row). |
-| Cylindrical rare-earth (neodymium) magnet, ≥ 10 mm diameter, weatherproof or epoxy-coated (e.g. [Adafruit 9](https://www.adafruit.com/product/9)) | 1 | Actuator for the reed switch above. Mounts to the coupler pin, knuckle carrier, or adjacent structural member such that the magnet is within the reed switch's rated operating distance when the coupler is closed. Verify magnet-to-switch operating distance against the chosen switch's datasheet before mounting. |
-| Solar charge controller, 6 V panel input, single-cell LiPo output, MPPT (e.g. CN3065-based module, 450 mA max charge) | 1 | Manages CC/CV LiPo charging from the solar panel with input-side MPPT. **Never connect the solar panel directly to the LiPo terminal.** The controller charges the LiPo; a separate 5 V boost module (see row above) then supplies the Scoop J1 input — the charge controller itself does not need a USB/5 V output port. |
-| Solar panel, 6 V, 3–5 W, rigid monocrystalline (e.g., [Voltaic Systems P103C](https://voltaicsystems.com/3-5-watt-panel/), 3.5 W) | 1 | Mounts flat on car roof. Feeds the charge controller's solar input. A 3–4 W panel is sufficient for the default 15-minute sample / 4-hour status cadence in mid-latitude summer; match to your charge controller's maximum input current rating. |
-| LiPo battery, 3.7 V single cell, 2000–4000 mAh (e.g., [Adafruit 2011](https://www.adafruit.com/product/2011), 2000 mAh) | 1 | Primary energy storage. Connects to the charge controller battery output and to the 5 V boost module input. The Notecarrier CX receives operating power through the Scoop J2 (OUT) connector, not directly from the LiPo. Size to ≥2000 mAh for all-cellular corridors; use 4000 mAh for primarily satellite corridors or northern-latitude winter deployments. |
-| GNSS magnetic-mount antenna, SMA, 3 m lead, 1575 MHz (L1), multi-band preferred (e.g., [SparkFun GPS-14986](https://www.sparkfun.com/products/14986)) | 1 | Routes from the Notecard GPS u.FL port to the car roof via the u.FL-to-SMA adapter below. Must cover at minimum GPS L1 (1575.42 MHz); multi-band L1/L2/L5 coverage (1164–1610 MHz) is preferred. Both antennas must have a clear, unobstructed sky view. |
-| u.FL (IPEX/MHF1) to SMA female bulkhead pigtail cable, 100–200 mm, RG178 coaxial | 1 | Adapts the Notecard GPS u.FL port to the SMA GNSS antenna above. Route the cable inside the enclosure; the SMA female end mates to the antenna's SMA male connector through a panel-mount SMA bulkhead feedthrough in the enclosure wall. A 100 mm pigtail is typical; ensure routing clears sharp edges and the feedthrough is rated for the antenna's frequency range. |
-| NEMA 4X polycarbonate enclosure, ~8×6×3″ | 1 | Weatherproof housing for all electronics. Rated for outdoor rail-car use; cable glands for antenna leads, reed switch wire, pressure fitting, and power wiring. |
-
-Notecard for Skylo ships with bundled cellular and satellite connectivity — 500 MB cellular data and 10 KB satellite data included, with no activation fees and no monthly commitment. The Notecarrier CX is a carrier board and does not contain a SIM.
-
-## 5. Wiring and Assembly
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. Notecard for Skylo seats into the carrier's M.2 slot; its MAIN u.FL port connects to the included Skylo-certified LTE/NTN antenna, and its GPS u.FL port connects to the separate GNSS antenna via the u.FL-to-SMA pigtail adapter listed in the BOM. The Mojo sits inline between Scoop J2 and the Notecarrier `+VBAT` during bench testing (remove for field deployment).
-
-
-
-**Power chain (solar → charge controller → LiPo → 5 V boost → Scoop inline → Notecarrier):**
-
-- Solar panel `+` / `−` → charge controller solar input terminals.
-- Charge controller battery output JST → LiPo battery JST connector. The controller manages CC/CV charging and over-charge protection; **never connect the solar panel directly to the LiPo terminal.**
-- LiPo battery JST → 5 V boost module input. The boost module steps the LiPo's 3.0–4.2 V up to a regulated 5 V output.
-- 5 V boost module output → Scoop **J1 (CHG)** JST connector (or the **J3** through-hole header pins, which are electrically identical to J1). Scoop's CHG input requires ≥ 4.8 V; the 5 V boost module satisfies this requirement. **Do not wire J1 directly to the LiPo** (max 4.2 V, below the 4.8 V minimum) or to the bare solar panel output.
-- Scoop **J2 (OUT)** JST connector (or the **J4** through-hole header pins, electrically identical to J2) → Notecarrier CX `+VBAT` JST connector. J2 delivers 2.5–3.8 V — within the LiPo-range input accepted by the Notecarrier CX. The internal LiC charges from J1 between radio sessions and discharges through J2 to supplement the supply rail during high-current cellular and satellite bursts. Scoop is the **only** path from the power chain to the Notecarrier `+VBAT`; the LiPo does not connect directly to the Notecarrier.
-- **Mojo (bench only):** Insert Mojo inline between Scoop J2/J4 and the Notecarrier — Scoop J2/J4 → Mojo `BAT` input JST → Mojo `LOAD` output JST → Notecarrier CX `+VBAT` JST. Then connect a Qwiic cable from either Mojo Qwiic port to a Qwiic port on the Notecarrier CX; Mojo reports cumulative mAh over Qwiic, and the Notecard auto-detects it (firmware v8 and later) — see the [Mojo datasheet](https://dev.blues.io/datasheets/mojo-datasheet/) for details on how the data surfaces. Remove Mojo and restore the direct Scoop J2/J4 → Notecarrier CX `+VBAT` connection before field deployment.
-
-**I²C bus (SDA / SCL pins on Notecarrier CX header):**
-
-The Notecarrier CX has on-board I²C pull-ups. All I²C devices share the bus without conflict:
-
-| Device | I²C Address | Connection |
-|--------|-------------|------------|
-| Notecard for Skylo | 0x17 | Internal (M.2 slot) |
-| ADXL345 breakout | 0x53 | SDA / SCL header; SDO pin to GND |
-| MPRLS pressure sensor | 0x18 | SDA / SCL header; address is fixed — **TANK_CAR builds only** |
-
-Run a short Qwiic/STEMMA QT daisy chain or individual 4-wire (VCC/GND/SDA/SCL) connections from each breakout to the Notecarrier CX header.
-
-**1-Wire bus (DS18B20 cargo temperature probe — TANK_CAR builds only):**
-
-The DS18B20 uses a single-wire protocol on `D6`. Wire as follows:
-
-- DS18B20 **data** (yellow) wire → Notecarrier CX `D6`; also connect the **4.7 kΩ pull-up resistor** (BOM item) from the data line to `+3V3_OUT`.
-- DS18B20 **power** (red) wire → `+3V3_OUT`.
-- DS18B20 **GND** (black) wire → `GND`.
-
-Route the DS18B20's stainless-steel probe through a watertight cable gland in the enclosure wall and extend it into the lading compartment. The probe housing is rated for direct immersion; verify chemical compatibility with the specific lading before installation. See §5.1 Safety before planning any lading-compartment penetration.
-
-**Power to sensor breakouts:**
-
-- ADXL345 `VCC` → Notecarrier CX `+3V3_OUT`. MPRLS `VCC` → `+3V3_OUT` (**TANK_CAR builds only**). All sensors together draw < 5 mA in normal use (100 mA available on `+3V3_OUT`).
-- All sensor `GND` pins → Notecarrier CX `GND`.
-
-**Reed switch (coupler state):**
-
-- One reed switch lead → Notecarrier CX `D5`.
-- Other lead → `GND`.
-- Firmware configures `D5` as `INPUT_PULLUP`; reed switch closed (magnet present = coupled) pulls the pin LOW.
-
-**MPRLS pressure port:**
-
-The MPRLS breakout has a 1/8 NPT threaded port on the sensor body. For field deployment, connect this port via a short stainless-steel or brass fitting to a **low-pressure vent or access port on the car body** — not a pressurized cargo line or any fitting carrying hazardous contents. Mount the sensor board inside the electronics enclosure; run the fitting through the enclosure wall with a sealed bulkhead union. Verify that the fitting material is compatible with any vapors that may be present. See §5.1 Safety before planning any pressure port installation.
-
-### 5.1 Safety Considerations
-
-
-
-**Read before installing.** Failure to follow the guidance below could result in personal injury, property damage, regulatory violations, or Skylo network exclusion.
-
-
-
-- **Certified antenna.** Notecard for Skylo ships with a Skylo-certified LTE/NTN antenna. Using any other antenna invalidates the Skylo certification and may result in Skylo blocking the device from its NTN network. Do not substitute another antenna without obtaining a delta test lab report through a CTIA/OTA-authorized facility. Contact [Blues](https://blues.com/contact-sales/) for recommended test houses.
-
-- **Pressure port safety.** The Adafruit MPRLS (product 3965) is a consumer-grade breakout with a 25 PSI absolute upper limit. It must **not** be plumbed into a pressurized cargo line, any fitting carrying hazardous or flammable contents, or any port that may exceed its 25 PSI absolute rating. Installation of any fitting or sensor on a regulated tank car must be performed by qualified personnel following all applicable DOT, AAR, and carrier/operator rules. For production cargo pressure sensing, replace the MPRLS with a certified industrial transducer rated for the lading and pressure class (see [§11](#11-limitations-and-next-steps)).
-
-- **Solar power wiring.** Never connect the solar panel output directly to the LiPo battery. Always route the panel through the specified charge controller (or an equivalent CC/CV controller rated for the panel and battery). Operating a LiPo without proper charge control can cause fire, venting, or permanent battery damage.
-
-- **Rail-car installation.** All work on in-service rail cars must comply with applicable AAR, FRA, and carrier rules. This reference design is a bench proof-of-concept. Mounting hardware to the car structure, routing wiring through the car body, or connecting to the car's fittings requires authorization from the car owner or lessor and must follow all applicable federal and industry regulations.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) (format: `com.your-company.your-name:rail-tracker`) and paste it into the `PRODUCT_UID` macro in `firmware/rail_car_tracker/rail_car_tracker_helpers.h`.
-
-2. **Claim the Notecard.** Power the assembled unit. Notecard for Skylo attempts cellular connection on first boot and associates itself with the project automatically. The device appears in the **Devices** tab within a few minutes. If cellular coverage is unavailable at the bench, wait until an antenna is connected and the unit has sky view.
-
-3. **Set the device serial number.** In Notehub, open the device and set the **Serial Number** field to the car's reporting mark and number (e.g., `UTLX-123456`). This propagates through every event and makes it trivial to filter events by car in downstream analytics.
-
-4. **Create Fleets.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. Natural fleet boundaries for a rail car fleet: one fleet per lessee, one per car type (tank, flat, boxcar), or one per geographic corridor. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can auto-assign devices based on location or car metadata using rule-based logic.
-
-5. **Set environment variables.** In Notehub, navigate to **Project → Fleets** → [create or choose a fleet] → **Environment**. (Alternatively, **Project → Devices → [device] → Environment** for per-device overrides.) All values below are optional; firmware defaults apply if not set. Changes are pulled by the device on its next inbound sync — no reflash required.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_min` | `15` | Minutes between sensor samples (and host wakes). |
- | `report_interval_min` | `240` | Minutes between `railcar_status.qo` summary Notes. |
- | `location_interval_min` | `30` | Maximum gap (minutes) between `railcar_location.qo` position Notes while the car is moving. Has no effect on motion-state-edge Notes — those fire on every wake where a stopped ↔ moving transition is detected, regardless of this interval. Because the host only wakes on the `sample_interval_min` cadence, a transition that occurs between wakes can be detected and reported up to `sample_interval_min` minutes after it occurs; reduce `sample_interval_min` to narrow this window at the cost of battery life. |
- | `shock_threshold_g` | `2.5` | Peak resultant G above which an impact is counted and, after cooldown, an alert is sent. The 2.5 G default is a threshold on total resultant vector magnitude (`√(Gx²+Gy²+Gz²)`), which includes the ~1 G static gravity component. Because the firmware does not apply gravity compensation or high-pass filtering, the equivalent net dynamic impact at this threshold depends on sensor mounting orientation relative to the impact direction — use empirical per-install calibration with observed baseline readings in `railcar_status.qo` rather than simple subtraction to interpret this value. Adjust up for cars with robust draft gear or down for sensitive cargo. |
- | `shock_cooldown_min` | `5` | Minimum minutes between consecutive shock alert Notes. Prevents alert storms when a car moves through a rough stretch of track. |
- | `pressure_max_psi` | `20.0` | (**TANK_CAR builds only.**) Fitting absolute pressure (PSI) above which a `pressure_high` alert fires. Standard atmospheric pressure at sea level is ~14.7 PSI absolute — set this threshold above the expected fitting operating pressure. Firmware clamps this variable to 25 PSI to match the MPRLS absolute range. |
- | `pressure_drop_psi` | `10.0` | (**TANK_CAR builds only.**) A drop from the previous absolute-pressure reading exceeding this value (PSI) fires a `pressure_drop` alert, indicating a possible leak or sudden valve event. |
- | `tank_temp_min_c` | `-10.0` | (**TANK_CAR builds only.**) Cargo low-temperature alert threshold (°C). Fires the `tank_temp_low` alert when the DS18B20 probe reading falls below this value. Firmware clamps the threshold to the range −60–25 °C. |
- | `tank_temp_max_c` | `50.0` | (**TANK_CAR builds only.**) Cargo high-temperature alert threshold (°C). Fires the `tank_temp_high` alert when the DS18B20 probe reading exceeds this value. Firmware clamps the threshold to the range 20–100 °C. |
-
-6. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `railcar_alert.qo` (to an on-call endpoint, CMMS, or webhook), a second for `railcar_status.qo` (to a long-term analytics store), and a third for `railcar_location.qo` (to a geofencing service or location time-series store. See [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for supported route types). The three Notefiles are deliberately separate so high-urgency alerts, periodic condition telemetry, and the dense position stream don't share a routing path — each can be independently throttled, transformed, or forwarded.
-
-### What you should see in Notehub
-
-In **Devices → [device] → Events** (or **Project → Devices → Events** depending on UI generation), you'll see:
-
-- **`_session.qo`** — Notecard session housekeeping, one per cellular or NTN connection. Appears first, within ~2–5 minutes on the bench if cellular is available. Confirms the radio is reaching Notehub. Only used for Blues diagnostics — you can ignore it.
-
-- **`railcar_status.qo`** — emitted immediately on first boot, then once per `report_interval_min` (default 240 minutes = 4 hours). Sample body (Note: GPS coordinates are injected from the Notecard's most recent fix via the `_lat`/`_lon`/`_ltime` compact template fields and do not appear in the host-side payload):
- ```json
- {
- "_lat": 41.4993,
- "_lon": -81.6944,
- "_ltime": 1713888000,
- "coupled": true,
- "moving": false,
- "shock_peak_g": 1.3,
- "shock_windows": 0
- }
- ```
- **Field meanings:** `coupled` is the latest reed-switch state (true = magnet present, coupled). `moving` is whether the Notecard's internal accelerometer detected motion in this sample window. `shock_peak_g` is the highest peak resultant G-force magnitude detected across all sample bursts since the previous summary (reported 0.0 if ADXL345 is missing or all reads failed). `shock_windows` is the count of **sample windows whose peak exceeded `shock_threshold_g`**, not a total impact count; see [§7.3](#73-sensor-reading-strategy). For TANK_CAR builds, `pressure_psi` (fitting absolute pressure) and `tank_temp_c` (cargo temperature) are added to this template. A value of `-9999` in `pressure_psi` or `tank_temp_c` means the sensor did not initialize on that wake — treat as a transient sensor fault.
-
-- **`railcar_alert.qo`** — emitted only when a threshold is exceeded, with an immediate `hub.sync` request. The `alert` field is one of: `impact`, `coupled`, `decoupled`, `pressure_high`, `pressure_drop`, `tank_temp_low`, `tank_temp_high` (last four in TANK_CAR builds only). Sample alert on a coupling impact:
- ```json
- {
- "alert": "impact",
- "value": 3.8,
- "_lat": 41.4993,
- "_lon": -81.6944,
- "_ltime": 1713888000
- }
- ```
-
-- **`railcar_location.qo`** — emitted on two triggers: (1) a motion-state edge (stopped ↔ moving) detected on the host's wake cadence — detection can lag the actual transition by up to `sample_interval_min` minutes; once detected, a `hub.sync` is requested immediately within the same wake; (2) while moving, every `location_interval_min` minutes (default 30 minutes). Sample body:
- ```json
- {
- "_lat": 41.4993,
- "_lon": -81.6944,
- "_ltime": 1713888000,
- "moving": true,
- "coupled": true
- }
- ```
- This provides downstream geofencing services with both position and operational context (moving/coupled) needed to detect railroad boundary crossings and coupler changes at interchange points. GPS coordinates are injected from the Notecard's last known fix; no host query is needed.
-
-## 7. Firmware Design
-
-**Three-file sketch.** All three files must reside in the same sketch directory to compile:
-
-| File | Role |
-|---|---|
-| [`rail_car_tracker.ino`](firmware/rail_car_tracker/rail_car_tracker.ino) | `setup()` / `loop()` orchestration; global object definitions (`Notecard`, `PersistState`) |
-| [`rail_car_tracker_helpers.h`](firmware/rail_car_tracker/rail_car_tracker_helpers.h) | Compile-time constants, `#define TANK_CAR` build flag, `PersistState` struct, `extern` declarations, function prototypes |
-| [`rail_car_tracker_helpers.cpp`](firmware/rail_car_tracker/rail_car_tracker_helpers.cpp) | All Notecard interactions, sensor reads, and note-emission helpers |
-
-### 7.0 TANK_CAR build flag
-
-The firmware compiles into two distinct profiles controlled by a single `#define` in `rail_car_tracker_helpers.h`:
-
-```cpp
-// Uncomment to enable tank-car pressure monitoring:
-// #define TANK_CAR
-```
-
-| | Standard build (default, `TANK_CAR` commented out) | TANK_CAR build |
-|---|---|---|
-| **Sensors initialized** | ADXL345, reed switch | + MPRLS pressure sensor, DS18B20 cargo temperature probe |
-| **Fields in `railcar_status.qo`** | `coupled`, `moving`, `shock_peak_g`, `shock_windows` | + `pressure_psi`, `tank_temp_c` |
-| **Alert types** | `impact`, `coupled`, `decoupled` | + `pressure_high`, `pressure_drop`, `tank_temp_low`, `tank_temp_high` |
-| **Env vars consumed** | `sample_interval_min`, `report_interval_min`, `location_interval_min`, `shock_threshold_g`, `shock_cooldown_min` | + `pressure_max_psi`, `pressure_drop_psi`, `tank_temp_min_c`, `tank_temp_max_c` |
-| **BOM additions** | None | Adafruit MPRLS breakout (product 3965), Adafruit DS18B20 waterproof probe (product 381), 4.7 kΩ pull-up resistor |
-
-In the default standard build, the MPRLS is never initialized, the `pressure_psi` field is absent from every Note template (saving satellite bytes), and the pressure alert logic is compiled out entirely; no "MPRLS not found" warnings appear on non-tank assets. Enable `TANK_CAR` only when the MPRLS sensor is physically fitted.
-
-
-
-**Switching build profiles.** The firmware encodes the build profile into `CONFIG_VERSION` (standard = 4, TANK_CAR = 104). Toggling `#define TANK_CAR` therefore automatically invalidates the stored configuration on the Notecard and forces `defineTemplates()` to re-register the correct schema on the next wake — no manual version bump is needed. Without this coupling, flipping the flag while keeping the same `CONFIG_VERSION` would leave a stale `railcar_status.qo` template that either lacks or spuriously includes `pressure_psi` and `tank_temp_c`, causing `note.add` to reject payloads whose schema does not match the registered template.
-
-
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Add the board index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**, then install "STM32 MCU based boards." Select **Blues Cygnet** as the board target (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-- **`Blues Wireless Notecard`** (`note-arduino`). Install via the Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`. See [note-arduino releases](https://github.com/blues/note-arduino/releases) for available versions.
-- **`Adafruit MPRLS Library`** — **TANK_CAR builds only.** Install via Library Manager (`arduino-cli lib install "Adafruit MPRLS Library"`). Not required for standard (non-tank) builds.
-- **`OneWire`** — **TANK_CAR builds only.** Install via Library Manager (`arduino-cli lib install "OneWire"`). Provides the 1-Wire bus driver used by the DS18B20 probe.
-- **`DallasTemperature`** — **TANK_CAR builds only.** Install via Library Manager (`arduino-cli lib install "DallasTemperature"`). High-level API for Dallas/Maxim DS18B20 temperature sensors over the OneWire bus.
-
-The ADXL345 is driven by direct I²C register reads using the built-in `Wire` library — no additional library required.
-
-**Flashing via `arduino-cli`:**
-
-```bash
-# Install the STM32 core if you haven't already
-arduino-cli core install STMicroelectronics:stm32
-
-# Verify the Cygnet board is available
-arduino-cli board listall | grep -i cygnet
-
-# Compile
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/rail_car_tracker/
-
-# Upload (adjust /dev/cu.usbmodem* for your system; on Linux it may be /dev/ttyACM*)
-# When the device appears in the list, use the actual port
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/rail_car_tracker/
-```
-
-**Serial console during a successful boot:**
-
-```
-[notecardReady] OK
-[configureNotecard] OK
-[defineTemplates] OK
-[configureMotionAndGPS] OK
-[sample] wake 1: coupled=1 moving=0 shock_peak_g=0.9 shock_windows=0
-[sendSummary] OK
-[loop] sleeping 15 minutes...
-(host quiet for 15 minutes)
-[sample] wake 2: coupled=1 moving=0 shock_peak_g=1.1 shock_windows=0
-```
-
-After the first sample cycle, the host powers off until the next ATTN fire (default 15 minutes). Serial output going quiet is **expected behavior, not a hang.** Use the timing shown above to verify the sample interval. Open a serial monitor at **115200 baud** (e.g., `screen /dev/cu.usbmodem* 115200` on Mac/Linux, or Arduino IDE's Serial Monitor).
-
-### 7.2 Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard readiness | `notecardReady` — per-boot I²C cold-boot retry before any transaction |
-| Notecard configuration | `configureNotecard` — `hub.set` with voltage-variable sync; returns `bool` |
-| Note templates | `defineTemplates` — compact templates for all three Notefiles; returns `bool` |
-| Motion and GPS config | `configureMotionAndGPS` — Notecard accelerometer + location mode; returns `bool` |
-| Env var fetch | `fetchEnvOverrides` — pull and clamp all environment variables per wake |
-| Coupler debounce | `readCouplerState` — 5-sample majority vote |
-| Shock scoring | `adxl345Begin`, `adxl345ReadG`, `readPeakShockG` — 64-sample burst; I²C validated per read |
-| Alert emission | `sendAlert` — compact Note; returns `bool`; sync coalesced via `hub.sync` after all alerts |
-| Summary emission | `sendSummary` — latest sensor readings + shock window accumulators |
-| Location emission | `sendLocationNote` — compact position Note to `railcar_location.qo`; fired on motion-state edges and `location_interval_min` cadence while moving |
-| Sleep | `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` — separate restore/save descriptors; return values checked |
-
-### 7.3 Sensor reading strategy
-
-- **ADXL345 shock scoring.** The sensor is configured for ±16 G full-resolution mode (3.9 mg/LSB). Each sample cycle reads 64 samples at ~100 Hz (~640 milliseconds burst) and tracks the peak resultant vector magnitude `√(Gx²+Gy²+Gz²)`. At rest, this reads ~1.0 G (static gravity). An impact registers as a spike above that baseline. Because the firmware compares total resultant magnitude, not gravity-compensated acceleration, the relationship between the 2.5 G threshold and the net dynamic impact depends on sensor mounting orientation: the gravity vector's contribution to the resultant is not simply subtractable. Empirical per-install calibration using observed baseline readings in `railcar_status.qo` is the reliable way to set `shock_threshold_g` for a specific installation. **`shock_windows` counts sample windows, not individual impacts.** The 640 milliseconds burst is read once per 15-minute wake: an impact that occurs between bursts is invisible to this firmware. `shock_windows` is therefore the number of wakes in the summary period where the burst peak exceeded `shock_threshold_g`; it can be zero when real impacts occurred outside sample windows. Do not interpret it as a total impact count. Each I²C read in the burst is validated; if the repeated-start write fails or fewer than 6 bytes are returned, the individual sample is discarded rather than allowed to produce a spurious magnitude spike. If all reads in a burst fail, `peakG` returns `NAN` and neither `shock_peak_g` accumulation nor `shock_windows` is incremented for that cycle.
-- **MPRLS.** Single I²C measurement via the Adafruit library. `readPressure()` returns pressure in **hPa** (absolute); the firmware divides by 68.948 to convert to PSI absolute. Valid range 0–25 PSI absolute (0–1723 hPa); accuracy ±0.25 % FSS typical. At standard sea-level conditions a port open to atmosphere reads approximately 14.7 PSI absolute; a pressurized fitting reads above that baseline. A sharp drop in absolute pressure toward atmospheric may indicate a leak or valve event; a reading significantly below atmospheric likely indicates a sensor wiring fault. This sensor is not rated for full DOT-111 or higher-class tank pressure. See [§11](#11-limitations-and-next-steps).
-- **DS18B20 cargo temperature probe (TANK_CAR builds).** Initialized via the DallasTemperature library over the OneWire bus on `D6`. Configured at 12-bit resolution (0.0625 °C step). Each wake calls `requestTemperatures()`, which blocks approximately 750 milliseconds for the conversion, then reads the result with `getTempCByIndex(0)`. The library returns `DEVICE_DISCONNECTED_C` (−127 °C) when the probe is absent or wiring is broken; the firmware treats any value below −100 °C as an error and stores `NAN`, which is reported as `−9999` in `tank_temp_c`. Sensor accuracy is ±0.5 °C across the −10 to +85 °C range. Verify chemical compatibility of the stainless-steel probe housing with the lading before installation.
-- **Reed switch.** Five `digitalRead` samples with 20 milliseconds spacing; majority vote (≥ 3 of 5 agreeing) determines the accepted state. Edge detection against the persisted previous state triggers coupler-change alerts.
-
-### 7.4 Event payload design
-
-All three Notefiles use [compact Note templates](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates), which is required for satellite (NTN) transport and dramatically reduces per-Note byte count over cellular as well. The `_lat`, `_lon`, and `_ltime` compact reserved fields restore GPS coordinates into the otherwise stripped compact template; the Notecard injects the most recent fix automatically, no host GPS query needed for summary Notes. The `railcar_status.qo` template body occupies approximately 28 bytes in a standard build (add ~4 bytes each for `pressure_psi` and `tank_temp_c` in TANK_CAR builds, totalling ~36 bytes). This is the compact Note **body size only**, not the total satellite data consumption per Note. Real NTN delivery also incurs session-establishment overhead, routing metadata, and delivery receipts. See [§11 Limitations](#11-limitations-and-next-steps) for satellite budget guidance.
-
-The `railcar_status.qo` body carries the most recent sensor readings from the sample cycle that triggered the summary, plus `shock_peak_g` (highest G seen since the previous summary) and `shock_windows` (number of **sample windows** in the summary period whose burst peak exceeded `shock_threshold_g`). `shock_windows` is a sampled-threshold window count, not a count of individual impacts. See [§7.3](#73-sensor-reading-strategy). This is not a window average of all samples; it is the latest single reading plus accumulated extremes.
-
-Sample `railcar_alert.qo` body (GPS coordinates are injected from the Notecard's last known fix via `_lat`/`_lon`/`_ltime` compact template fields, they do not appear in the host-side `note.add` call):
-
-```json
-{
- "file": "railcar_alert.qo",
- "body": {
- "alert": "impact",
- "value": 3.8,
- "_lat": 41.4993,
- "_lon": -81.6944,
- "_ltime": 1713888000
- }
-}
-```
-
-Alert types and their `value` fields:
-
-| `alert` | `value` | Build |
-|---|---|---|
-| `impact` | Peak resultant G of the triggering sample burst | All |
-| `coupled` | `1.0` (coupler closed) | All |
-| `decoupled` | `0.0` (coupler opened) | All |
-| `pressure_high` | Fitting absolute pressure (PSI) at alert time | TANK_CAR only |
-| `pressure_drop` | Magnitude of the absolute-pressure drop (PSI), not the current reading | TANK_CAR only |
-| `tank_temp_low` | DS18B20 cargo temperature (°C) that tripped the threshold | TANK_CAR only |
-| `tank_temp_high` | DS18B20 cargo temperature (°C) that tripped the threshold | TANK_CAR only |
-
-### 7.5 Low-power strategy
-
-The host Cygnet STM32L433 is fully powered off between samples via `card.attn` sleep mode. Following the pattern of the reference accelerators, all sensing and logic runs in `setup()`; `loop()` holds only the `NotePayloadSaveAndSleep` call and a fallback `delay`. `NotePayloadSaveAndSleep` serializes the `PersistState` struct into Notecard flash, then issues the `card.attn` sleep request that cuts the host power rail for `sample_interval_min × 60` seconds. On ATTN fire, the Notecarrier CX re-applies host power, the MCU enters `setup()` from cold, and `NotePayloadRetrieveAfterSleep` rehydrates the struct. The host is awake for only the few seconds needed to read sensors, evaluate rules, and queue Notes: on the order of 5–10 seconds per 15-minute interval.
-
-Notecard for Skylo idles at ~8–18 µA @ 5V between sessions (see the [low-power firmware design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)). GPS is motion-gated: `card.location.mode` with `threshold: 1` keeps the GNSS radio off while the car sits in a yard, waking it only when the Notecard's internal accelerometer detects movement. Outbound sync cadence adapts to battery charge state via `voutbound`/`vinbound` voltage-variable strings:
-
-| Voltage tier | `voutbound` interval | `vinbound` interval |
-|---|---|---|
-| USB | 60 minutes | 120 minutes |
-| High | 120 minutes | 240 minutes |
-| Normal | 240 minutes | 480 minutes |
-| Low | 480 minutes | 720 minutes |
-| Dead | Paused (0) | Paused (0) |
-
-At high charge (good solar harvest), the Notecard syncs every 2 hours; at normal charge, every 4 hours; at low charge, every 8 hours. At "dead" voltage, outbound syncs are suspended to protect the battery. On first boot, the firmware sends a summary Note immediately regardless of the report interval — this ensures a known-good status lands in Notehub during commissioning.
-
-### 7.6 Retry and error handling
-
-- **Per-boot Notecard readiness.** `notecardReady()` issues a lightweight `card.version` via `sendRequestWithRetry(req, 10)` at the top of every `setup()` call, before any other Notecard transaction. The host MCU can power up before the Notecard's I²C stack is ready after every `NotePayloadSaveAndSleep` wake, not just on initial firmware flash. This ensures the bus is live before `fetchEnvOverrides`, `card.time`, and all other requests.
-- **One-time configuration retry.** `configureNotecard`, `defineTemplates`, and `configureMotionAndGPS` all return `bool`. `state.configured` is set `true` only if all three succeed. If any step fails, `state.configured` stays `false` and the next wake retries the full configuration sequence automatically.
-- **Response error-field checking.** All `requestAndResponse` calls check for `NULL` return and inspect the `err` field in the response JSON before treating the response as valid. `notecard.deleteResponse` is always paired with any non-NULL response. `sendRequest` / `sendRequestWithRetry` calls that return a `bool` are checked and logged on failure.
-- **Note emission error visibility.** `sendAlert` and `sendSummary` use `requestAndResponse` (not fire-and-forget `sendRequest`) so the Notecard's `err` field is visible on failure; dropped Notes are logged to serial.
-- **PRODUCT_UID runtime guard.** If `PRODUCT_UID` is empty at startup, the firmware logs a fatal message to serial and halts before attempting any Notecard communication, making the misconfiguration immediately obvious at the bench without requiring Notehub to diagnose a missing project association.
-- **NotePayload save-or-sleep failure.** `NotePayloadAddSegment` and `NotePayloadSaveAndSleep` return values are checked in `loop()`. If either fails, the firmware logs the error and issues an explicit `card.attn sleep` fallback request to preserve battery rather than leaving the host awake indefinitely.
-- **Pressure drop validity.** A separate `lastPressureValid` flag in `PersistState` tracks whether the stored `lastPressurePsi` came from a successful sensor read. The flag is cleared whenever a read returns `NAN`. A `pressure_drop` alert is suppressed unless both the previous and current readings are valid, preventing stale readings from manufacturing a false drop alert after one or more failed cycles.
-- **Summary timing stability.** The summary window is tracked as accumulated elapsed minutes (`state.elapsedMin += sampleMin` on each wake) rather than a wake count multiplied by the current interval. A runtime change to `sample_interval_min` therefore does not retroactively shift the window boundary.
-- **MPRLS / DS18B20 fault sentinels.** Reads that fail initialization return `NAN`; `sendSummary` replaces `NAN` with `-9999` in `pressure_psi` and `tank_temp_c` so downstream analytics can distinguish a sensor fault from a legitimate near-zero reading. The DS18B20 returns `DEVICE_DISCONNECTED_C` (−127 °C) when the probe is absent or wiring is broken; the firmware treats any value below −100 °C as an error and converts it to `NAN` before calling `sendSummary`. If the ADXL345 is absent or all reads in a burst fail, `peakG` is `NAN`; neither `shock_peak_g` accumulation nor `shock_windows` is incremented for that cycle.
-- **Alert sync coalescing.** `sendAlert` and `sendLocationNote` (on motion-state edges) do not set `sync:true` on individual `note.add` calls. After all alert and location logic completes for a wake, a single `hub.sync` is issued if any alert or motion-edge location Note was queued. This avoids redundant sync-session requests when multiple events fire in the same wake.
-- **Env-var clamping.** All values from `fetchEnvOverrides` are clamped before use — a malformed Notehub value can't produce an out-of-range sleep duration or division-by-zero interval.
-
-### 7.7 Key code snippet 1: compact template with GPS fields
-
-The `format:"compact"` argument is required for Notecard-for-Skylo NTN Notes. The `_lat`/`_lon`/`_ltime` keywords restore GPS coordinates into an otherwise stripped compact template; the Notecard injects the most recent fix automatically, no host GPS query needed for summary Notes. The `pressure_psi` and `tank_temp_c` fields are added only in TANK_CAR builds (wrapped in `#ifdef TANK_CAR`); the snippet below shows the standard (non-tank) template.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "railcar_status.qo");
-JAddNumberToObject(req, "port", 10);
-JAddStringToObject(req, "format", "compact");
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "_lat", TFLOAT32);
-JAddNumberToObject(body, "_lon", TFLOAT32);
-JAddNumberToObject(body, "_ltime", TINT32);
-// #ifdef TANK_CAR
-// JAddNumberToObject(body, "pressure_psi", TFLOAT32); // MPRLS fitting pressure (PSI abs)
-// JAddNumberToObject(body, "tank_temp_c", TFLOAT32); // DS18B20 cargo temperature (°C)
-// #endif
-JAddBoolToObject (body, "coupled", true);
-JAddBoolToObject (body, "moving", true);
-JAddNumberToObject(body, "shock_peak_g", TFLOAT32);
-JAddNumberToObject(body, "shock_windows", TINT16);
-notecard.sendRequest(req);
-```
-
-### 7.8 Key code snippet 2: voltage-variable hub.set
-
-`voutbound` adjusts the outbound sync interval based on the VBAT voltage reported by the Notecard — no host-side battery measurement needed. At `high` charge the Notecard syncs every 120 minutes; at `normal` every 240 minutes; at `low` every 480 minutes; at `dead` outbound is suspended entirely.
-
-```cpp
-J *req = notecard.newRequest("hub.set");
-JAddStringToObject(req, "product", PRODUCT_UID);
-JAddStringToObject(req, "mode", "periodic");
-JAddStringToObject(req, "voutbound",
- "usb:60;high:120;normal:240;low:480;dead:0");
-JAddStringToObject(req, "vinbound",
- "usb:120;high:240;normal:480;low:720;dead:0");
-notecard.sendRequestWithRetry(req, 10);
-```
-
-### 7.9 Key code snippet 3: ADXL345 shock scoring burst
-
-64 reads at ~100 Hz gives a ~640 milliseconds window — long enough to capture a coupling impact transient. The resultant vector magnitude includes the static gravity component (~1.0 G at rest). Because the firmware does not compensate for gravity, translating a 2.5 G threshold into a "net impact" figure depends on the angle between the gravity vector and the impact direction. Use empirical per-install calibration rather than simple arithmetic to set `shock_threshold_g` for your installation. Each I²C read is validated; failed reads are skipped so a short I²C transfer can't inject a spurious magnitude. The function returns `NAN` if no reads succeed.
-
-```cpp
-float peakG = 0.0f;
-uint8_t validSamples = 0;
-for (uint8_t i = 0; i < 64; i++) {
- float gx, gy, gz;
- if (!adxl345ReadG(gx, gy, gz)) { delay(10); continue; }
- float mag = sqrtf(gx*gx + gy*gy + gz*gz);
- if (mag > peakG) peakG = mag;
- validSamples++;
- delay(10);
-}
-return (validSamples > 0) ? peakG : NAN;
-```
-
-### 7.10 Key code snippet 4: persist state and sleep
-
-`NotePayloadSaveAndSleep` is a `note-arduino` helper that writes the state struct to Notecard flash and issues `card.attn` sleep — cutting host power entirely. On the next ATTN fire the MCU re-enters `setup()` cold; `NotePayloadRetrieveAfterSleep` rehydrates the struct in the first few lines.
-
-Restore and save use **separate** `NotePayloadDesc` objects. The restore descriptor is stack-allocated in `setup()` and freed with `NotePayloadFree` after the segment is read. The save descriptor is freshly zero-initialized in `loop()`, preventing any stale segment state from being carried into the next sleep cycle. Both return values are checked; a failed save falls back to an explicit `card.attn sleep` request rather than leaving the host awake.
-
-```cpp
-// In setup(): restore
-NotePayloadDesc restorePayload;
-bool wakeFromSleep = NotePayloadRetrieveAfterSleep(&restorePayload);
-if (wakeFromSleep) {
- wakeFromSleep &= NotePayloadGetSegment(&restorePayload, STATE_SEG_ID,
- &state, sizeof(state));
- NotePayloadFree(&restorePayload);
-}
-
-// In loop(): save
-NotePayloadDesc savePayload = {0, 0, 0};
-bool segOk = NotePayloadAddSegment(&savePayload, STATE_SEG_ID, &state, sizeof(state));
-bool sleepOk = segOk && NotePayloadSaveAndSleep(&savePayload, g_sampleMin * 60U, NULL);
-if (!sleepOk) { /* fallback card.attn sleep */ }
-```
-
-## 8. Data Flow
-
-
-
-**Collected every `sample_interval_min` (default 15 min):** coupler state (boolean), peak resultant G in the sampling burst, Notecard motion state (moving/stopped from internal accelerometer); TANK_CAR builds also collect low-pressure fitting absolute pressure (PSI) and DS18B20 cargo temperature (°C).
-
-**Transmitted:**
-
-- `railcar_status.qo` — one compact Note **generated** per `report_interval_min` (default every 4 hours; also immediately on first boot). **Generation and delivery are separate steps.** The Cygnet host creates the Note and queues it to the Notecard on the `report_interval_min` cadence. The Notecard **delivers** queued Notes on the next outbound sync session, scheduled by the voltage-variable `hub.set` at 2 hours (high charge), 4 hours (normal charge), or 8 hours (low charge), or the next time the Notecard can establish an NTN session with adequate sky view when cellular is unavailable. Contains `coupled`, `moving`, `shock_peak_g`, `shock_windows`; TANK_CAR builds also include `pressure_psi` and `tank_temp_c`. GPS coordinates injected automatically by the Notecard from the most recent fix via the `_lat`/`_lon`/`_ltime` compact template fields.
-- `railcar_alert.qo` — emitted on any threshold trip; a single `hub.sync` is issued after all alerts for the wake are queued, requesting immediate delivery. The Notecard transmits over cellular if available; if not, the Note waits in flash until the next satellite NTN window or the next time cellular coverage opens.
-- `railcar_location.qo` — emitted on two independent triggers: (1) a motion-state edge (stopped ↔ moving) detected on the host's `sample_interval_min` wake cadence — a transition that occurs between wakes is detected and reported on the next wake, up to `sample_interval_min` minutes later; once detected, a `hub.sync` is requested immediately within the same wake so the yard-arrival or yard-departure Note reaches Notehub without waiting for the next scheduled outbound window; (2) while moving, every `location_interval_min` minutes (default 30 min, adjustable via env var), filling the gap between periodic status summaries with a dense enough position record for interchange-boundary determination. Body contains only `moving` and `coupled`; `_lat`/`_lon`/`_ltime` are injected automatically by the Notecard from the last known GPS fix. GNSS runs every 5 minutes while the car is moving (motion-gated to save battery during yard dwell), so in-motion position fixes are refreshed well within the default 30-minute location cadence.
-
-**Routed:** all three Notefiles land in Notehub. `railcar_alert.qo` routes to a real-time endpoint (webhook, email, or CMMS); `railcar_status.qo` routes to a time-series store for trend analysis and car utilization reporting; `railcar_location.qo` routes to a geofencing service or location time-series store for interchange-boundary detection.
-
-**Alert triggers (three always-on + four TANK_CAR-only):**
-
-| Alert | Condition | Repeat behavior | Build |
-|---|---|---|---|
-| `impact` | Peak G ≥ `shock_threshold_g` | Rate-limited: one alert per `shock_cooldown_min` window; prevents storms on rough track | All |
-| `coupled` / `decoupled` | Coupler-state edge from previous sample | Edge-detected: one alert per state transition, regardless of how long the state persists | All |
-| `pressure_high` | Fitting absolute pressure > `pressure_max_psi` | Edge-detected: fires once when condition is first observed; suppressed while it persists | TANK_CAR only |
-| `pressure_drop` | Fitting pressure fell > `pressure_drop_psi` since the previous sample | One alert per detected sudden-drop event; suppressed unless both the previous **and** current readings are valid (prevents stale readings from producing false alerts after a failed cycle) | TANK_CAR only |
-| `tank_temp_low` / `tank_temp_high` | DS18B20 cargo temperature outside `tank_temp_min_c` / `tank_temp_max_c` | Edge-detected: fires once when condition is first observed; suppressed on subsequent samples while it persists; re-arms when temperature returns inside range | TANK_CAR only |
-
-## 9. Validation and Testing
-
-**Expected steady-state cadence.** A correctly installed unit generates one `railcar_status.qo` every `report_interval_min` (default 4 hours) and delivers it on the next scheduled sync session. At normal battery voltage over cellular, generation and delivery are both on a 4-hour cadence. At low voltage, the sync interval stretches to 8 hours and Notes may queue for that duration before delivery. `railcar_location.qo` fires on every detected motion-state-edge and, while moving, every `location_interval_min` minutes (default 30 min). Motion-state edges are detected on the host's 15-minute wake cadence — a stopped ↔ moving transition can be reported up to `sample_interval_min` minutes after it occurs; once detected, a `hub.sync` is requested immediately within the same wake. To reduce the detection window, lower `sample_interval_min` (via env var) at the cost of battery life. Zero `railcar_alert.qo` events is normal during smooth transit. During initial commissioning, review the `shock_peak_g` values in the first several `railcar_status.qo` Notes to establish the car's baseline vibration signature, then tune `shock_threshold_g` and the pressure-drop threshold accordingly. The firmware does not implement automatic calibration or baseline learning — commissioning-time threshold tuning is a manual step using observed data.
-
-**Power validation with Mojo.** The [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) sits inline on the VBAT rail during bench bring-up and reports cumulative mAh to the Notecard over Qwiic (see [§5](#5-wiring-and-assembly) for the full bench wiring).
-
-The table below lists per-phase reference figures drawn from the [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) and the [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/), alongside whole-system bench estimates for this assembly. Read the two columns differently: the reference column reflects what Blues has published; the bench estimate column is a starting point to confirm with Mojo.
-
-| Phase | Reference figure | Source | Whole-system bench estimate |
-|---|---|---|---|
-| Deep sleep: host off, Notecard idle (radio off) | ~8–18 µA @ 5V | [Low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) | ~20–50 µA total @ 3.7 V |
-| Sample cycle: host + sensors active (~5–10 s) | — | — | ~30–40 mA |
-| Cellular sync — average current | Modem active: ~250 mA | [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | ~150–300 mA average; session ~15–60 s typical |
-| Cellular sync — burst peak | Up to ~2 A for a few ms (GSM fallback regions) | [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | Battery, connectors, wiring, charge path, and Scoop buffer must all be validated against the full published peak burst-current envelope (~2 A); validate with Mojo traces before finalizing sizing — do not use a headroom figure below the peak you cite |
-| NTN satellite sync (Skylo session) — average current | No NOTE-NBGLWX NTN-specific figure published; modem-active baseline: ~250 mA | [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | **Bench estimate only:** sustained current during session likely at or above cellular average; session duration minutes-scale; validate with Mojo before finalizing sizing |
-| NTN satellite sync — burst peak | Not published for NOTE-NBGLWX NTN operation | — | Size for at least the same headroom as cellular; treat any pre-Mojo estimate as a floor, not a ceiling |
-
-**Estimated daily energy budgets** — whole-system estimates only; **always measure with Mojo before finalizing battery and panel sizing**:
-
-| Scenario | Daily consumption | Notes |
-|---|---|---|
-| All-cellular, mostly-stationary (car in yard) | 15–25 mAh / 24 h | GNSS motion-gated off. Host sleeps 15 min / wake cycle for ~5–10 s. Sync every 4 h at normal charge. **Best-case baseline for yard-dwell scenarios.** |
-| All-cellular, in-transit (car moving hours/day) | 40–80 mAh / 24 h | GNSS active every 5 min while moving. `railcar_location.qo` fires every 30 min + on motion edges. GNSS duty cycle adds significant budget. Validate empirically with Mojo under representative duty cycle. |
-| Mixed NTN/cellular or predominantly NTN | 80–200+ mAh / 24 h | NTN session overhead dominates. Sessions last 1–4 min at high current; exact budget depends on coverage in your corridor. **Measure with Mojo in your target deployment zone before sizing.** |
-
-**Commissioning guidance for 2000 mAh LiPo + 3.5 W solar panel (US mid-latitude, summer):** A stationary car in a yard should sustain indefinitely on a 3.5 W panel and 2000 mAh battery (daytime solar recharges faster than baseline drain). An in-transit car burning 50 mAh/day should also sustain on the same panel in good summer sun; in winter or at higher latitudes, increase panel size to 5–6 W or battery to 4000 mAh. NTN-heavy corridors should validate consumption with Mojo before final sizing — satellite sessions can dominate the budget unexpectedly.
-
-**Mojo trace patterns to look for:**
-
-- **Healthy:** flat near-zero baseline with brief blips at the 15-minute sample interval and one longer burst (cellular or satellite) at the sync interval.
-- **Host not sleeping:** continuous 30–50 mA baseline. Usually a `card.attn` / `NotePayloadSaveAndSleep` misconfiguration; verify the Notecarrier CX ATTN pin is connected (it is internally on the CX; no external wire needed).
-- **Frequent satellite sessions:** very long (1–4 min) high-current bursts replacing the shorter cellular bursts. Normal behavior when the car is in a cellular-dark corridor.
-- **Rapid satellite data depletion:** check that compact templates are defined before the first `note.add`. A non-compact Note sent over NTN can cost several times more satellite bytes than a compact one.
-
-Mojo is a bench-validation and per-firmware-revision regression tool. Field units don't need it.
-
-## 10. Troubleshooting
-
-| Symptom | Diagnosis | Recovery |
-|---|---|---|
-| Device does not appear in Notehub **Devices** tab after 5 minutes | PRODUCT_UID not set, or set incorrectly. The Notecard claims itself to a ProductUID on first boot over cellular. | Check `firmware/rail_car_tracker/rail_car_tracker_helpers.h` line 40; confirm `PRODUCT_UID` matches **Project Settings → ProductUID** in Notehub. Re-flash. If cellular is unavailable at bench, the Notecard cannot claim itself — connect the LTE/NTN antenna and move near a window, or proceed indoors if NTN is available in your region. |
-| Serial monitor shows `[setup] FATAL: PRODUCT_UID is empty` then halts | PRODUCT_UID is empty or commented out. | Uncomment and set the ProductUID in `rail_car_tracker_helpers.h`. Re-flash. |
-| `_session.qo` appears but no `railcar_status.qo` for 20+ minutes | Host is not waking or not queuing Notes. Check three things: (1) Is the host entering sleep? The serial monitor should show `[sample]` once, go quiet for ~15 minutes (normal), then show `[sample]` again. If output is continuous, host is not sleeping — verify that `NotePayloadSaveAndSleep` is returning true. (2) Is the Notecard configured? On first boot, `[notecardReady]`, `[configureNotecard]`, `[defineTemplates]`, and `[configureMotionAndGPS]` should all log `OK`. If any fail, they retry on the next wake. (3) Is the sample interval too long? Default is 15 minutes; with `report_interval_min` at 240 minutes, the first status Note appears 4 hours after boot. Reduce `report_interval_min` to 15 via Notehub **Fleet → Environment** for faster feedback during commissioning. |
-| `railcar_status.qo` has `shock_peak_g: 0.0` always | ADXL345 is not connected or not responding on I²C. Check SDA/SCL wiring to header. The firmware logs `[adxl345Begin] OK` on startup if the sensor is found. If not present, shock is silently recorded as 0.0. | Verify Qwiic/STEMMA QT cable to ADXL345 breakout, or check 4-wire I²C connections (VCC/GND/SDA/SCL). Re-flash and watch serial output. |
-| `railcar_status.qo` has `pressure_psi: -9999` (TANK_CAR build) | MPRLS pressure sensor failed to initialize or returned an error on this wake. | Check SDA/SCL wiring to MPRLS breakout. Verify I²C address is 0x18 (default; no address pin jumpers on Adafruit 3965). Power-cycle the unit. If errors persist, check that TANK_CAR is enabled in `rail_car_tracker_helpers.h` and the MPRLS library is installed (`arduino-cli lib install "Adafruit MPRLS Library"`). |
-| `railcar_status.qo` has `tank_temp_c: -9999` (TANK_CAR build) | DS18B20 probe failed to initialize or wiring is broken. The firmware treats values below −100 °C as errors. | Verify the 4.7 kΩ pull-up resistor is connected between D6 and +3V3_OUT. Check the DS18B20 data (yellow) wire to D6, power (red) to +3V3_OUT, and GND (black) to GND. Power-cycle. If the probe has been run too long or the stainless-steel housing is corroded, replace the sensor. |
-| Device claims but never syncs (no Notes flow to Notehub) | Cellular and satellite coverage both unavailable; Notes queue in Notecard flash and wait for a window. Also check `hub.set` configuration. | Verify antenna connections: Notecard for Skylo's MAIN u.FL must connect to the Skylo-certified LTE/NTN antenna (included with the Notecard), and the GPS u.FL connects to the separate GNSS antenna via u.FL-to-SMA pigtail. Both antennas need clear sky view. If both are connected and sky-view is clear, check the Notecard console in Notehub (**Devices → [device] → Notecard Console**) for `[hub]` errors. |
-| Mojo shows high baseline current (30–50 mA continuously) | Host is not sleeping. The Notecarrier CX ATTN pin controls host power gating; it must be connected (internal on CX, no external wire needed). If something is holding the host awake, the baseline will not drop to sleep levels (~20–50 µA). | Check the serial log for `[loop]` — if it appears more frequently than once per 15 minutes, the host is looping instead of sleeping. Verify `NotePayloadSaveAndSleep` returns true and issues a `card.attn sleep` request in the log. A failed power-save will leave the host consuming ~30–50 mA continuously. |
-| Rapid satellite session bursts (1–4 minutes high-current events) | Cellular coverage is lost; the Notecard is falling back to Skylo NTN. This is expected and correct behavior. | No action needed — this is the reference design working as intended. Monitor satellite data consumption in Notehub to confirm you're within your NTN budget. Validate compact templates are being used before the first `note.add` call — a non-compact Note over NTN costs 3–10× more bytes. |
-
-## 11. Limitations and Next Steps
-
-A car-level tracker that has to live for years on solar power, ride out weeks in cellular-dark corridors, and report condition without false alarms is a deeply tunable problem. The list below is the deliberate scope boundary for this reference design: the places we kept the implementation simple so the queue-and-forward architecture is easy to read, plus the natural extensions for a fleet-grade deployment.
-
-### Simplified for this POC
-
-These are the spots where the implementation was kept simple to keep the queue-and-forward architecture readable; each comes with the consideration a production deployment should weigh.
-
-**The satellite data budget cannot be projected from body sizes.** Compact Note templates substantially reduce per-Note payload: `railcar_status.qo` occupies approximately 28 bytes of body content per Note (add ~8 bytes for `pressure_psi` and `tank_temp_c` in TANK_CAR builds, totalling ~36 bytes), `railcar_alert.qo` approximately 24 bytes, and `railcar_location.qo` approximately 20 bytes. However, these are **body-only, template-only sizes**; they do not represent end-to-end satellite data consumption. Real Skylo NTN usage also includes session-establishment overhead, routing metadata, delivery receipts, and any retries, and session overhead can dominate the budget before raw body bytes become a concern, especially at frequent sync cadences or when alert traffic is high. **Do not use body-size arithmetic to project allowance endurance.** Validate actual satellite byte consumption in Notehub under your intended sync cadence and expected alert behavior before sizing a production satellite plan. In practice, a car on a US rail corridor spends much of its time in cellular range at yards and populated corridors, preserving most of the 10 KB bundled allowance for the truly remote stretches.
-
-**The shock threshold is total vector magnitude, not gravity-compensated.** The resultant magnitude at rest reads ~1.0 G (static gravity). The 2.5 G threshold applies to total `√(Gx²+Gy²+Gz²)`. Because the firmware does not apply gravity compensation or high-pass filtering, this threshold cannot be converted to a "net impact" figure by simple subtraction; the relationship depends on the angle between the gravity vector and the impact direction. Gravity compensation or high-pass filtering would be required in firmware to make the threshold orientation-independent; alternatively, use empirical per-install calibration. For cars with active vibration (e.g., empty tank cars resonating on corrugated track), the threshold may need raising to 3–4 G to suppress nuisance alerts. Tune via the `shock_threshold_g` env var after observing baseline readings in `railcar_status.qo`.
-
-**Cargo temperature is single-point only (TANK_CAR builds).** The DS18B20 probe provides a single temperature measurement at the probe tip. For ladings with large internal temperature gradients, or where regulatory compliance requires multi-point temperature verification, additional probes or a certified cargo temperature system are needed. The probe accuracy (±0.5 °C, −10 to +85 °C range) is sufficient for basic thermal monitoring of most common bulk liquid ladings but should be validated against the specific lading and temperature range.
-
-**Pressure monitoring is low-pressure fitting only; there is no tank gauge pressure.** The Adafruit MPRLS covers 0–25 PSI absolute (roughly 0–10 PSI above atmospheric at sea level). The `pressure_max_psi` env var is firmware-clamped to 25 PSI to match this absolute range. DOT-111 non-pressure cars can operate at up to ~100 PSI gauge; DOT-105 and pressure cars are higher still — all require a certified industrial pressure transducer of the appropriate type (gauge, absolute, or differential) with matching wetted-parts spec for the lading.
-
-**Solar power requires a charge controller.** The BOM includes a solar charge controller as a required component. **Do not substitute a direct panel-to-LiPo connection.**
-
-**Coupler state is sampled, not interrupt-driven.** A coupling or decoupling event that occurs and reverses entirely within a single 15-minute sample window will not be detected. Reducing `sample_interval_min` to 5 minutes narrows this gap at the cost of battery life.
-
-**Interchange detection is not implemented on-device.** Determining that a car has changed railroad custody — the core of interchange tracking — requires either on-device geofencing against railroad territory boundary polygons, or downstream processing against a geospatial database of those boundaries. This firmware does neither: it reports GPS location, motion state, coupler state, and sensor readings, but contains no handoff-detection logic. Production interchange tracking further requires feeding detected boundary crossings to the AAR's Umler or Railinc EDI platforms as structured interchange transactions. Both the geofencing step and the EDI integration step are production work outside the scope of this POC. See Production Next Steps below.
-
-**There is no GNSS in covered yards.** A car parked under an overhead conveyor structure or inside a building will lose GPS fix. The Notecard retains the last known position but location accuracy will degrade until sky view is restored.
-
-### Production Next Steps
-
-These extensions take the tracker from a readable reference build toward fleet-grade condition reporting and formal interchange integration.
-
-**Field-calibrate `shock_threshold_g` per car type.** A tank car with a hand brake applied resonates differently than an empty flatcar; a single fleet-wide threshold is a starting point, not a final answer.
-
-**Add additional DS18B20 probes** on the same OneWire bus (up to ~10 devices per bus segment) for multi-point cargo temperature profiling; use `getDeviceCount()` and `getTempCByIndex(n)` in the DallasTemperature library to iterate all sensors.
-
-**Replace the MPRLS with a 4–20 mA industrial pressure transmitter** (e.g., Honeywell STS3000 series) rated for the lading and pressure class; read via a 250 Ω burden resistor on an analog input.
-
-**Wire a second reed switch** to a hatch or valve cover for tank cars to detect unauthorized access to the dome.
-
-**Implement [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** so threshold recipe updates and new alert rules can be pushed to the fleet over the air without a truck roll.
-
-**Connect Notehub location events to a geofencing service** or location time-series store (via a [Notehub route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub)) to detect railroad territory boundary crossings and generate interchange events for each custody handoff.
-
-**Feed those interchange events to an AAR/Railinc EDI gateway** (Umler or Railinc platforms) for automated interchange reporting.
-
-## 12. Summary
-
-The lessor who used to wait on an EDI interchange report now hears from the tank car itself. The same Texas-to-New-Jersey trip that used to spend three days in a Tennessee yard with nothing more than a paperwork acknowledgment is now a quarter-hourly health check, a continuous GPS track, a coupling-impact score for every hard hit, and an alert the moment fitting pressure drops or temperature drifts out of range. The continuous GPS track, coupler-state transitions, and motion events that reach Notehub are precisely the inputs a downstream geofencing service needs to detect railroad boundary crossings and generate interchange records; the geofencing and EDI integration steps that convert that telemetry into formal interchange events are production work outlined in [§11 Production Next Steps](#11-limitations-and-next-steps). Status Notes are generated every 4 hours and delivered on the next sync session: within 2–4 hours over cellular at good charge, or the next time the Notecard can establish an NTN session across the open Nebraska Sandhills. The key insight is that Notecard for Skylo doesn't ask the firmware to choose a transport; it manages cellular and satellite autonomously, queues everything that can't transmit immediately, and flushes it the moment connectivity returns. For an asset class where "no news" has historically meant "no visibility," that queue-and-forward guarantee is the entire value proposition.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/rail-car-condition-interchange-tracker/)**
diff --git a/78-rail-car-condition-interchange-tracker/banner.png b/78-rail-car-condition-interchange-tracker/banner.png
deleted file mode 100644
index 6f0b010f..00000000
Binary files a/78-rail-car-condition-interchange-tracker/banner.png and /dev/null differ
diff --git a/78-rail-car-condition-interchange-tracker/diagrams/01-system-architecture.svg b/78-rail-car-condition-interchange-tracker/diagrams/01-system-architecture.svg
deleted file mode 100644
index 137c1148..00000000
--- a/78-rail-car-condition-interchange-tracker/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/78-rail-car-condition-interchange-tracker/diagrams/02-wiring-assembly.svg b/78-rail-car-condition-interchange-tracker/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 279f5e29..00000000
--- a/78-rail-car-condition-interchange-tracker/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,75 +0,0 @@
-
diff --git a/78-rail-car-condition-interchange-tracker/diagrams/03-data-flow.svg b/78-rail-car-condition-interchange-tracker/diagrams/03-data-flow.svg
deleted file mode 100644
index 20069004..00000000
--- a/78-rail-car-condition-interchange-tracker/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
diff --git a/79-utility-distribution-transformer-load-monitor/README.md b/79-utility-distribution-transformer-load-monitor/README.md
index 00edc00d..f9ddcac1 100644
--- a/79-utility-distribution-transformer-load-monitor/README.md
+++ b/79-utility-distribution-transformer-load-monitor/README.md
@@ -1,555 +1,5 @@
# Utility Distribution Transformer Load Monitor
-
+A cellular energy monitoring solution that gives utilities real-time load visibility at the distribution transformer, tracking per-phase loading, imbalances, and thermal stress.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Utility%20Distribution%20Transformer%20Load%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/transformer_load_monitor/](firmware/transformer_load_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is an [energy monitoring](https://blues.com/solutions-energy-monitoring/) solution that gives utilities real-time load visibility at the pole — where transformer failures actually happen. A [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) with three split-core current transformers on the secondary terminals and an I²C temperature sensor tracks per-phase loading, detects dangerous current imbalances, and correlates load with enclosure temperature as a thermal-stress proxy, all over cellular, with no site infrastructure required.
-
-## 1. Project Overview
-
-**The problem.** A distribution transformer is the last powered device between the high-voltage grid and a customer's service entrance. Utilities typically instrument their substations carefully (real-time SCADA, demand data, fault recorders), but the transformer hanging on the pole outside a neighborhood has none of that. The utility knows what's flowing into the substation; it doesn't know what's flowing through any individual transformer until something fails.
-
-That ignorance is expensive. Distribution transformers age and fail for identifiable, preventable reasons: sustained overloading (insulation degrades with each degree of excess temperature), phase imbalance (uneven single-phase loads on a three-phase transformer create circulating currents and localized hot spots), and thermal stress from the combination of high ambient temperature and high loading. None of these conditions are sudden. They develop over hours to days, and a transformer that is running hot on a Tuesday will fail during Friday's peak demand, taking down a neighborhood and requiring an emergency truck roll that costs many times what proactive replacement would have.
-
-This project puts an end to that ignorance. A handful of sensors clamped to the transformer secondary, a [Notecard](https://shop.blues.com/products/notecard-cellular?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) for the uplink, and a threshold-checking firmware loop turn an opaque pole-top asset into a continuously-monitored piece of equipment that tells the operations center what's happening in real time and pages the right people before the neighborhood goes dark.
-
-**Why Notecard.** Utility poles are among the least connected pieces of infrastructure there is. They have no Ethernet, no building WiFi, and often nothing except the transformer's own secondary power. Cellular is the only practical backhaul. The Notecard Cell+WiFi variant (MBGLW) carries a prepaid SIM with 500 MB of data and 10 years of service: no per-site activation, no carrier contracts to negotiate, no IT ticket to file for network access. A utility deploying these monitors across its service territory can use a single SKU on every pole in every neighborhood, from dense urban blocks to rural feeders. The Notecard's automatic carrier selection handles varied network conditions without firmware changes.
-
-
-
-The power picture tells the same story. Cellular removes the site infrastructure dependency entirely. The Notecard Cell+WiFi idles at [~18 µA @ 5V](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) between samples while the host MCU is fully powered off, and the 5-minute sample / hourly transmit cadence means the radio wakes for roughly 20–30 seconds per hour, well within what a small AC/DC supply tapped from the transformer's own secondary can comfortably sustain.
-
-**Deployment scenario.** A NEMA 4X weatherproof enclosure zip-tied or U-bolted to the transformer pole, powered from a small AC/DC supply connected to the 120VAC secondary, with three split-core CT leads exiting through weatherproof cable glands and clamped directly onto the secondary conductors.
-
-CT installation is **non-invasive**: the split-core sensors clamp directly onto energized secondary conductors without breaking the circuit. The power supply connection is a separate matter — tapping the 120VAC secondary is live utility electrical work; depending on utility policy and local jurisdiction, this step may require a scheduled outage, a qualified hot-work procedure, or written utility approval before the tap is energized (see [§11 Limitations](#11-limitations-and-next-steps)). No invasive tap into the transformer case and no on-site network provisioning are required. A two-person crew, including an electrician qualified for live secondary work, can install and commission one unit in under an hour once all necessary work authorizations are in hand.
-
-
-
-**Power supply scope.** This reference design assumes an accessible 120VAC secondary tap — the standard case for North American single-phase distribution transformers installed by a qualified crew. Three-phase padmount transformers, sealed primary-side enclosures, or installations where utility rules prohibit tapping the secondary require an alternate power arrangement. See [§11 Limitations](#11-limitations-and-next-steps) for the inductive harvesting alternative.
-
-
-
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The host on the pole has a simple, repeating job. Every 5 minutes (configurable) the Cygnet STM32L4 on the Notecarrier CX wakes, reads the configured CT channels — two for the common split-phase install, three when `phase_count=3` is set for three-phase — reads the enclosure-internal I²C temperature sensor, runs three threshold checks in firmware, and queues the results as [Notes](https://dev.blues.io/api-reference/glossary/#note) via the Notecard over I²C. The moment that's done it goes back to sleep. Between wakes the host is completely powered off through the Notecard's [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) mechanism, with the host power rail held low until the wake interval expires and ATTN brings the rail back up. All the state that has to survive that gap (summary accumulators, alert cooldown counters, elapsed time) is serialized to Notecard flash via `NotePayloadSaveAndSleep` before each sleep and restored with `NotePayloadRetrieveAfterSleep` on the next wake.
-
-**Notecard responsibilities.** The Notecard takes everything the host just queued and decides when it goes over the air. Notes sit in its on-device queue and flush on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes), but any Note flagged `sync:true` (every alert) bypasses that timer and ships immediately. The Notecard also handles [environment variable](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) distribution from the [Blues Notehub](https://blues.com/notehub/) cloud service, so an operations engineer can retune thresholds across the fleet without re-flashing firmware; the device picks them up on the next inbound sync. (GNSS is available on supported hardware if a future revision needs one-time asset location, but it isn't used in this design — enabling it has antenna, power, and cost implications not covered here.)
-
-**Notehub responsibilities.** Once a Note leaves the pole, the Notecard's embedded global SIM carries it over supported carriers worldwide and delivers it to [Notehub](https://notehub.io), where events are ingested, stored, and routed downstream. Two Notefiles carry everything the device says: `xfmr_summary.qo` for hourly templated rollups suitable for long-term trending, and `xfmr_alert.qo` for event-triggered, immediate-delivery Notes. Organizing devices into [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) by service territory or transformer rating lets engineers set threshold [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) that apply across an entire group (all 25-kVA residential units on one feeder share one threshold profile) while still allowing per-device overrides for unusual transformers. [Smart Fleet rules](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can automatically assign a Notecard to the correct fleet based on its reported data.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP Pub/Sub, Snowflake, and other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint. Alert and summary Notefiles are deliberately separate so each can be routed independently: alerts to an outage-management or on-call system with low-latency delivery, summaries to a historian or analytics store with higher-volume, batched ingest.
-
-
-## 3. Technical Summary
-
-**What you need:**
-- Notecarrier CX + Notecard Cell+WiFi (MBGLW)
-- 3× YHDC SCT-013-000 CTs + 3.5 mm jack breakouts
-- Per-CT burden circuit: 22 Ω, 2× 10 kΩ, 10 µF cap (see §5)
-- MCP9808 I²C temperature sensor
-- 5V/2A AC/DC supply (e.g., MeanWell IRM-10-5)
-- Computer with Arduino IDE or `arduino-cli`
-
-**Bench validation (no live transformer needed):**
-1. Assemble the three CT bias circuits on A0, A1, A2 (see §5 wiring diagram).
-2. Connect MCP9808 to I²C (SDA/SCL).
-3. Flash firmware: set `PRODUCT_UID` in `transformer_load_monitor.ino` to your Notehub project ID, then upload via USB.
-4. Open serial monitor at 115200 baud. You should see `[sample] i_a=...` lines every 5 minutes.
-5. Create a project in [Notehub](https://notehub.io) if you haven't — the Notecard auto-associates on first cellular connect.
-6. After 60 minutes you'll see `[summary] queued` on serial. Wait for the device's next outbound sync (default hourly) and check Notehub **Devices → Events** for an `xfmr_summary.qo` entry.
-7. Clamp a CT on a known 120VAC load (a lamp works well) and verify the current value in the summary matches the expected load (within ~5%).
-
-
-Here is a sample Note this device emits:
-
-```json
-{
- "file": "xfmr_alert.qo",
- "body": {
- "alert": "phase_imbalance",
- "i_a_rms": 91.4,
- "i_b_rms": 34.2,
- "i_c_rms": 88.7,
- "temp_c": 44.1,
- "extra": 62.6
- }
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with onboard Cygnet STM32L4 host — three ADC-capable analog pins for the CT channels, I²C for the temperature sensor, and ATTN-based host power control for deep sleep. No separate MCU needed. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular removes per-pole IT dependency; one SKU with a prepaid global SIM spans urban and rural feeders without carrier contracts or per-site activation. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(bench / commissioning only, optional)* | 1 | Coulomb counter for bench power-budget validation. Not shipped with the pole-mount unit — stays on the bench during commissioning. Optional; use to verify the device achieves expected current draw before field deployment. See [§9](#9-validation-and-testing) for the expected current trace to look for. |
-| YHDC SCT-013-000 split-core CT, 100A / 50mA | 3 | Non-invasive, clip-on **current-output** CT. The 100A / 50mA variant covers residential distribution transformers up to approximately 20 kVA at 240V secondary (~83A full-load). For transformers at or above 25 kVA at 240V (~104A at full load), use a higher-range current-output CT; see the Note below. Interfaces directly with the resistor bias circuit; no integrator stage required. Widely available from Mouser and Amazon. |
-| 22 Ω 1% resistor (burden, 1 per CT) | 3 | Converts the 50mA CT output to ~1.1V RMS at 100A — squarely within the Cygnet ADC's 0–3.3V input range after biasing. |
-| 10 kΩ 1% resistor (bias divider, 2 per CT) | 6 | Creates the Vcc/2 (~1.65V) DC bias point that centers the AC CT signal within the ADC input range. |
-| 10 µF electrolytic capacitor (1 per CT) | 3 | AC-couples the CT signal to the bias node; blocks DC from the bias network leaking back into the CT. |
-| 3.5mm TRRS audio jack breakout (1 per CT) | 3 | The SCT-013-000 terminates in a 3.5mm plug; a breakout board avoids cutting the cable. |
-| [Adafruit MCP9808 High Accuracy I2C Temperature Sensor Breakout #1782](https://www.adafruit.com/product/1782) | 1 | ±0.25°C accuracy over –40°C to +125°C, I²C bus, 2.7–5.5V supply — ideal for enclosure thermal monitoring. No additional analog pins consumed. |
-| AC/DC supply, 5V/2A (e.g. [MeanWell IRM-10-5](https://www.meanwell.com/Upload/PDF/IRM-10/IRM-10-SPEC.PDF)) | 1 | Compact PCB-mount supply derives 5V DC from the transformer's 120VAC secondary. The IRM-10-5 (10W, 2A continuous) provides adequate rating to sustain cellular sessions that can peak at ~2A; the smaller IRM-05-5 (1A) is insufficient for reliable cellular operation. |
-| 2A 250V slow-blow fuse, 5×20 mm (e.g. Littelfuse 218002) | 1 | Protects the 120VAC tap to the IRM-10-5 supply. Required on the Line leg; install in a panel-mount or PCB fuse holder rated for 250VAC. |
-| 5×20 mm fuse holder, 250VAC rated | 1 | Mounts the slow-blow fuse on the Line conductor inside the enclosure. |
-| Cellular stub antenna *(bench / commissioning only, included with Notecarrier CX dev kit)* | 1 | Adequate for bench bring-up and indoor testing. Not suitable for permanent outdoor pole-mount deployment. |
-| Outdoor cellular antenna, 700–2700 MHz, SMA *(deployment)*, e.g., [SparkFun CEL-16432](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html) | 1 | Replaces the bench stub for installed units; choose a part rated for outdoor temperature range and UV exposure. See [§5](#5-wiring-and-assembly) for enclosure-dependent placement guidance. |
-| u.FL-to-SMA bulkhead pigtail, ≈100 mm, e.g., Taoglas CA-05250003 or equivalent | 1 | Routes the Notecard's u.FL antenna port to the externally-mounted SMA antenna. Required for all pole-mount deployments using the outdoor SMA antenna; also serves as the bulkhead lead through the enclosure wall for metal enclosures. |
-| NEMA 4X enclosure, ~6×4×2″ | 1 | Weatherproof housing for outdoor pole-mount. Fiberglass or polycarbonate variants are lighter than steel and don't attenuate the cellular antenna. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-
-
-**CT current range Note.** The SCT-013-000 rated at 100A suits transformers up to approximately 20 kVA at 240V secondary (~83A full-load, with ~17% headroom to the CT's 100A rated output). A common 25 kVA unit draws ~104A at full load — 4% above the CT's rating; use a higher-range current-output CT for transformers at or above 25 kVA. Larger transformers (50–167 kVA) draw 200–400A and require a higher-range **current-output** CT such as the YHDC SCT-036-200 (200A/50mA); update `CT_TURNS_RATIO` and `CT_BURDEN_OHMS` in firmware accordingly. Voltage-output CTs (such as the YHDC SCT-019-200, which produces 0.333V full-scale from an internal burden) are **not** drop-in replacements — they do not interface with the external bias circuit above and require a different analog front end. See [§11 Limitations](#11-limitations-and-next-steps) for the production path.
-
-
-
-
-
-**POC protection scope.** The BOM above provides primary overcurrent protection (the slow-blow fuse) and nothing else on the 120VAC mains entry. For a bench prototype or controlled field trial this is adequate starting hardware, but it is **not a complete protection story for a permanent outdoor pole-mount installation.** A production design must add: a surge protective device (SPD) rated for the installation's overvoltage category (Category C / lightning-level, per IEC 61643-11 or UL 1449) on the Line/Neutral entry; enclosure grounding and bonding to the pole ground system per applicable NEC sections and utility rules; verified creepage and clearance in all mains-voltage wiring and connectors; UV and temperature qualification of all external cabling and the enclosure; and written utility approval before the secondary tap is permanently energized. Component selection for all of the above is installation-specific and outside this POC's scope. See [§5](#5-wiring-and-assembly) and [§11](#11-limitations-and-next-steps) for further discussion.
-
-
-
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi (MBGLW) seats into the carrier's M.2 slot. The Mojo, if used for bench validation, sits inline between the 5V supply and the Notecarrier's +VBAT pad; connect its Qwiic cable to any available Qwiic port on the Notecarrier so it can log charge to the Notecard. The transformer firmware does not poll the Mojo — its coulomb-count readings appear as separate Notecard events in Notehub and are a bench commissioning tool only; the Mojo is not installed in the deployed unit.
-
-### Bias circuit (build once per CT channel, identical for A, B, and C)
-
-Each CT channel requires a small passive network that converts the CT's AC current to a voltage centered at Vcc/2 so it sits squarely within the Cygnet ADC's 0–3.3V input range.
-
-```
-(per channel — repeat for A0, A1, A2)
-
- +3V3
- │
- R1 10 kΩ ← top half of bias divider
- │
- ├─────────────────── ADC pin (A0 / A1 / A2)
- │ │
- R2 10 kΩ ← C 10 µF (+ plate toward ADC pin)
- │ │
- GND CT+ (TRRS tip)
- │
- Rb 22 Ω ← burden resistor
- │
- CT– (TRRS sleeve) ──── GND
-```
-
-**How it works:**
-- **Burden (Rb, 22 Ω):** The SCT-013-000 is a current-output CT; Rb converts its 50 mA RMS rated output to approximately 1.1 V RMS (≈1.56 V peak swing) at 100 A RMS primary current. Rb is wired directly across the CT output terminals (tip to sleeve).
-- **Bias divider (R1 + R2, 10 kΩ each):** Splits +3.3V to establish a DC bias of ≈1.65V (Vcc/2) at the ADC pin. This centers the AC signal within the ADC's single-supply input range.
-- **Coupling capacitor (C, 10 µF electrolytic):** AC-couples CT+ to the ADC pin. The positive plate faces the ADC pin (higher DC potential, ≈1.65 V); the negative plate faces CT+ (≈0 V DC). C passes the AC signal to the ADC pin while blocking DC from disrupting the bias point.
-- Each CT channel needs its own R1, R2, C, and Rb — do not share bias nodes or burden resistors across channels.
-
-### Pin-by-pin connections
-
-- **+3V3** → R1 top leg of each bias circuit; MCP9808 `VDD`.
-- **GND** → R2 bottom leg of each bias circuit; CT– (TRRS sleeve) of each jack; MCP9808 `GND`.
-- **A0** → bias-divider mid-point (junction of R1 and R2) and positive plate of C for the Phase-A channel. The negative plate of C connects to CT+ (TRRS tip). CT+ also connects to one leg of Rb (22 Ω); the other leg of Rb connects to GND / CT–.
-- **A1** → same circuit, Phase-B CT.
-- **A2** → same circuit, Phase-C CT. For split-phase installations (L1/L2 on A0/A1), leave A2 unconnected — the firmware defaults to `phase_count=2`, so no env var change is needed and the unconnected channel is never sampled. For three-phase installations, wire this bias circuit and set `phase_count=3` in Notehub before first power-on (see [§6](#6-notehub-setup)); without that setting, A2 is never read and imbalance calculations are incomplete.
-- **SDA** → MCP9808 `SDA` (Notecarrier CX has on-board 4.7 kΩ pull-ups on the I²C bus).
-- **SCL** → MCP9808 `SCL`.
-- **+VBAT** → Mojo `LOAD` output (bench only); Mojo `BAT` input → 5V from the IRM-10-5 supply.
-- **IRM-10-5 LINE/NEUTRAL** → 120VAC tapped from the transformer secondary. The Line conductor must pass through a 2A 250V slow-blow fuse (in a rated fuse holder) before reaching the supply.
-
-### Cellular antenna
-
-Connect the cellular antenna to the Notecard Cell+WiFi (MBGLW) u.FL port on the front edge of the Notecard. The Blues development kit includes a compatible stub antenna; for permanent pole-mount installations, a dedicated LTE antenna rated for outdoor use is preferred.
-
-Placement guidelines:
-- **Polycarbonate or fiberglass NEMA 4X enclosure:** the antenna can mount inside the enclosure on a standoff or against the lid. RF penetrates these materials with negligible loss.
-- **Steel or aluminum enclosure:** run a u.FL-to-SMA pigtail to a bulkhead-mount SMA antenna on the outside wall of the enclosure. Steel fully blocks the cellular signal from the inside.
-- Orient the antenna element vertically (parallel to the pole) for the best cellular radiation pattern.
-- Route the antenna cable away from the transformer core, primary conductors, and the 120VAC supply wiring to avoid coupling noise into the RF path.
-
-### 3.5mm TRRS jack wiring
-
-The SCT-013-000 uses a 3.5mm plug with tip = CT+ and sleeve = CT–. Wire the breakout board:
-- Tip → one end of the 22Ω burden (Rb) and to the AC coupling capacitor top plate
-- Sleeve → GND (and the other end of Rb)
-
-### CT installation
-
-Clamp each CT around a single secondary conductor, never around two conductors simultaneously, as the fields would cancel. For a single-phase (120/240V center-tap) transformer, clamp the Phase-A CT on the **X1** secondary conductor (L1 hot leg) and the Phase-B CT on the **X3** secondary conductor (L2 hot leg). **X2 is the center-tap neutral** — do not monitor X2 unless neutral current is an intentional additional metric; clamping one CT on X1 and the other on X2 would monitor one hot leg and the neutral, not the L1/L2 pair. For a three-phase transformer, clamp each CT on one secondary phase conductor. The third CT slot (A2/Phase-C) can be left unconnected for split-phase installations; the firmware defaults to `phase_count=2`, so no env var change is required. For three-phase installations, clamp the Phase-C CT on the third secondary conductor and set `phase_count=3` in Notehub before first power-on — without that setting, A2 is never sampled and any C-phase overload or imbalance contribution is invisible to the firmware.
-
-
-
-**CT open-circuit hazard.** The SCT-013-000 is a **current-output** CT. Never disconnect the 22Ω burden resistor or unplug a CT jack while the CT is clamped on an energized conductor. An open-circuited current-output CT can develop dangerous high voltages across its terminals. Always verify the burden resistor is in-circuit before clamping the CT onto a live conductor; always remove the CT from the conductor before disturbing the burden wiring or disconnecting the 3.5mm jack.
-
-**Safety.** Distribution transformer secondary conductors carry lethal voltages. Installation must be performed by qualified electrical workers with appropriate PPE, following utility safety procedures and applicable codes. This firmware is read-only — it never commands any switching of the transformer or its connected loads. The CTs are non-invasive and clamp without breaking the circuit, but proximity to energized conductors remains hazardous.
-
-**Surge and transient protection (POC omission).** The 2A slow-blow fuse protects the IRM-10-5 supply against a sustained short but does not suppress lightning-induced transients or switching surges, which are routinely present on distribution transformer secondaries. For bench bring-up this is acceptable; for any unit intended for permanent pole-mount deployment, install a surge protective device (SPD) rated for the site's overvoltage category (Category C / lightning-level, per IEC 61643-11 or UL 1449) across Line/Neutral ahead of the fuse holder. Bond the enclosure to the pole grounding system and verify conductor routing, creepage/clearance distances, and any utility-specific installation approval requirements before energizing the tap. See [§11](#11-limitations-and-next-steps) for the full list.
-
-
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) (format: `com.your-company.your-name:xfmr-monitor`) and set it as `PRODUCT_UID` in the firmware before flashing.
-
-2. **Claim the device.** Power the assembled unit. On first cellular connect, the Notecard associates with your project automatically — no manual claim required. The device appears in the **Devices** tab within a minute or two.
-
-3. **Create a Fleet per service territory.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. The natural unit is one fleet per transformer class or service territory — all 25-kVA residential transformers in a feeder get the same rated-amps threshold, while 50-kVA commercial units get a different one. Fleet-level [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) propagate to every device in the fleet without re-flashing.
-
-4. **Set environment variables.** In Notehub: **Fleet → Environment** (or **Device → Environment** for a per-unit override). All variables are optional for split-phase (L1/L2) installations — the firmware defaults are set for that common case. **Three-phase installations must set `phase_count=3` and force an inbound sync before live monitoring begins**; with the default `phase_count=2` on a three-phase transformer, the A2/Phase-C channel is never read, and any C-phase overload or imbalance contribution is invisible to the firmware. The device picks up all other env var changes on its next inbound sync (default every 2 hours) with no truck roll.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_sec` | `300` | Seconds between sensor readings (minimum 30). |
- | `summary_interval_min` | `60` | Minutes between summary Notes. Changing this also re-applies `hub.set` to match the Notecard's outbound cadence. |
- | `rated_amps` | `100.0` | Transformer secondary rated amps **per phase**. The default matches the 100A SCT-013-000 CT range. Set to the nameplate-derived secondary current (kVA rating ÷ secondary voltage), e.g., `83` for a 20-kVA / 240V unit or `62` for a 15-kVA / 240V unit. A 25-kVA / 240V transformer draws ~104A at full load, which exceeds the default CT's 100A rating; swap in a higher-range current-output CT and set this value to the transformer's actual nameplate current before deploying on transformers at or above 25 kVA. |
- | `overload_pct` | `95.0` | Any single phase exceeding this percentage of `rated_amps` triggers an `overload` alert. |
- | `imbalance_pct_thresh` | `20.0` | (max_phase − min_phase) / max_phase above this percentage triggers a `phase_imbalance` alert. Only evaluated when at least one phase is carrying >2A. |
- | `temp_alert_c` | `70.0` | Enclosure temperature (°C) above which a `high_temp` alert fires. Typical enclosure internal temperature limit is 50–65°C; 70°C is a conservative field alert. |
- | `alert_cooldown_sec` | `1800` | Minimum seconds between successive alerts of the same type (default 30 minutes). Prevents alert storms on a slow-drifting condition. |
- | `phase_count` | `2` | Number of active CT channels (1–3). The default `2` matches the common split-phase (L1/L2 on A0/A1) installation — no env var change needed out of the box. **Set to `3` for three-phase transformers** (A0/A1/A2 all wired) before first power-on; without this, A2 is never read. Set to `1` for single-leg monitoring. Imbalance is only meaningful across two or more phases; with `phase_count=1`, `imbalance_pct` is always reported as 0.0 in both real-time alerts and summary Notes. With `phase_count=2`, L1 and L2 are compared without the unconnected C channel (always 0A) inflating the imbalance reading. |
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `xfmr_alert.qo` (to an outage-management system, on-call paging, or CMMS endpoint) and a second for `xfmr_summary.qo` (to a long-term store or analytics platform). Keeping the two Notefiles separate means each can be fanned out to a different destination at a different urgency without filter logic in the route.
-
-### What you should see in Notehub
-
-**Note:** Template fields use numeric type hints to encode type and size: `14.1` = 4-byte IEEE-754 float; `12` = 2-byte signed integer. This compact binary format is decoded automatically by Notehub.
-
-- **`_session.qo`** — Notecard session housekeeping on each cellular connect. If this is showing up, the radio link is healthy.
-- **`xfmr_summary.qo`** — one per `summary_interval_min`. Annotated example payload (field comments are explanatory, actual JSON contains no comments):
- ```
- {
- "i_a_rms": 47.3, // phase-A mean RMS over loaded sample intervals only (A) — see §11
- "i_b_rms": 44.8, // phase-B mean RMS over loaded sample intervals (A)
- "i_c_rms": 49.1, // phase-C mean RMS over loaded sample intervals (A)
- "i_total": 141.2, // sum of per-phase loaded-interval means (A)
- "loading_pct": 39.2, // i_total / (rated_amps × phase_count) × 100; excludes idle intervals
- "imbalance_pct": 9.0, // (max_phase − min_phase) / max_phase × 100; from loaded-interval means
- "temp_c": 38.5, // mean enclosure temperature over valid temperature readings (°C)
- "overloads": 0, // sample intervals during which any phase exceeded the overload threshold
- "samples": 12, // loaded sample intervals (at least one phase above the noise floor)
- "total_wakes": 12 // all host wakes in the window (loaded + idle)
- }
- ```
- `overloads` counts **sample intervals** during which any phase exceeded the overload threshold, not distinct overload events. A single sustained overload lasting the full summary window at the default 5-minute sample rate would report `overloads: 12`. `samples` counts only intervals in which at least one phase was above the 0.5 A noise floor; `total_wakes` counts every host wake regardless of load. For time-weighted utilization analytics, multiply `loading_pct` by `samples / total_wakes` to account for idle intervals (see [§11](#11-limitations-and-next-steps)).
-- **`xfmr_alert.qo`** — emitted only on a threshold trip, transmitted immediately. The `alert` field is one of `overload`, `phase_imbalance`, or `high_temp`.
-
-
-## 7. Firmware Design
-
-The firmware lives in the sketch directory [`firmware/transformer_load_monitor/`](firmware/transformer_load_monitor/) and is split across three files that must be compiled together:
-
-| File | Role |
-|---|---|
-| [`transformer_load_monitor.ino`](firmware/transformer_load_monitor/transformer_load_monitor.ino) | Main sketch — `setup()`, `loop()`, global declarations |
-| [`transformer_load_monitor_helpers.h`](firmware/transformer_load_monitor/transformer_load_monitor_helpers.h) | Shared constants, structs, and function prototypes |
-| [`transformer_load_monitor_helpers.cpp`](firmware/transformer_load_monitor/transformer_load_monitor_helpers.cpp) | All helper function implementations |
-
-The Arduino IDE and `arduino-cli` automatically compile every `.ino`, `.h`, and `.cpp` file in the same sketch folder together — always open or target the **folder** (`firmware/transformer_load_monitor/`), not an individual source file.
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Add the board index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**, then install "STM32 MCU based boards." Select **Blues Cygnet** as the board (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-- **`Blues Wireless Notecard`** library ([`note-arduino`](https://github.com/blues/note-arduino)) — Install via the Arduino Library Manager (search "Blues Wireless Notecard") or `arduino-cli lib install "Blues Wireless Notecard"`.
-- **`Adafruit MCP9808 Library`** — install via the Arduino Library Manager (search "MCP9808"). Requires the Adafruit BusIO dependency, which the Library Manager installs automatically.
-
-**Flashing — Arduino IDE:** use **File → Open…** and select the folder `firmware/transformer_load_monitor/` (or double-click `transformer_load_monitor.ino`, the IDE loads all three files from the folder automatically). Select the Cygnet board, click **Upload**. The Notecarrier CX exposes the ST-Link interface on the same USB cable, so no external programmer is needed.
-
-**Flashing — `arduino-cli`:**
-```bash
-# Install the STM32 core and libraries first if not already done
-arduino-cli core install STMicroelectronics:stm32
-arduino-cli lib install "Blues Wireless Notecard" "Adafruit MCP9808 Library"
-
-# List installed Cygnet FQBNs for your specific core version
-arduino-cli board listall | grep -i cygnet
-
-# Compile and upload — target the sketch folder so all three source files
-# are included (substitute the FQBN reported above; /dev/cu.usbmodem* for Mac, /dev/ttyACM* for Linux)
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/transformer_load_monitor
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/transformer_load_monitor
-```
-
-After flashing, open the serial monitor at **115200 baud**. On first boot you should see `[sample] i_a=...` lines every `sample_interval_sec` seconds (default 5 minutes). After `summary_interval_min` minutes (default 60) you will see `[summary] queued (0 loaded / 12 total wakes)` if no CT is connected — the summary Note is queued to the Notecard's local on-device store at that point; it reaches Notehub on the next outbound cellular sync (default 60 minutes later). The summary is always emitted as a liveness heartbeat even with no load. When CTs are clamped on live conductors the sample lines show non-zero current values and the summary line reports the number of loaded intervals versus total wakes in that window.
-
-### 7.2 Firmware constants and environment variables
-
-The firmware defines several key tuning constants in `transformer_load_monitor_helpers.h`:
-
-| Constant | Default | Meaning |
-|---|---|---|
-| `CT_SCALE` | `(3.3/4096) × (2000/22)` | Converts raw ADC counts to primary amps. Numerator (2000/22) is the scale inverse of the 22 Ω burden at 100A CT rating; denominator 4096 is 12-bit ADC count range; 3.3V is supply. Use this formula as-is for the SCT-013-000 100A CT with 22 Ω burden. For higher-range CTs, recompute: (supply_v / adc_max_counts) × (ct_rated_ma / burden_ohms). |
-| `CT_TURNS_RATIO` | `1.0` | Nameplate ratio (primary amps / secondary amps). For SCT-013-000 = 1.0. Higher-range CTs change this; e.g., SCT-036-200 has ratio ~4.0. |
-| `CT_BURDEN_OHMS` | `22` | Burden resistor value. Change this only if you substitute a different burden; also update `CT_SCALE` if changed. |
-| `CT_NOISE_FLOOR_A` | `0.5` | Current values below this (in amps) are zeroed to suppress noise floor pickup. Increase to 1.0 if you see non-zero readings with no load. |
-| `CT_SAMPLE_PERIOD_US` | `225` | Pacing interval (microseconds) between ADC samples. 225 microseconds = ~4.44 kHz sampling; ~333 milliseconds window for RMS = ~20 mains cycles at 60 Hz. Do not change unless you understand the mains-cycle alignment issue described in §7.3. |
-
-### 7.3 Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard configuration (`hub.set`, `note.template`, `card.motion.mode`) | `hubConfigure`, `defineTemplates` |
-| Environment variable fetch + clamp | `fetchEnvOverrides`, `envFloat`, `envU32` |
-| Re-apply `hub.set` on summary-interval change | `fetchEnvOverrides` (static guard) |
-| CT RMS measurement (two-pass ADC sampling) | `readCtRms` |
-| Temperature reading via MCP9808 | `readTemperatureC` |
-| Alert threshold evaluation | `checkAlerts` |
-| Alert Note emission (`sync:true`) | `sendAlert` |
-| Hourly summary Note emission | `sendSummary` |
-| Persistent state save + host sleep | `NotePayloadSaveAndSleep` (library helper) |
-| State restore on wake | `NotePayloadRetrieveAfterSleep` (library helper) |
-
-### 7.4 Sensor reading strategy
-
-**CTs.** Each SCT-013-000 produces an AC current signal proportional to the line current flowing through its core. The external 22Ω burden resistor converts this to an AC voltage of approximately 1.1 V RMS (≈1.56 V peak) at 100 A RMS primary current, centered on the ADC's DC bias point (~1.65V = Vcc/2). The firmware uses a two-pass algorithm: 256 samples establish the actual DC offset (which may drift slightly from Vcc/2 due to component tolerance), then 1480 samples compute the RMS of the centered signal. Both passes pace samples to a fixed 225 microseconds period (≈4.44 kHz) using `micros()` and `delayMicroseconds()`, so the 1480-sample RMS window spans a deterministic ~333 milliseconds: approximately 20 mains cycles at 60 Hz, or ~16.7 cycles at 50 Hz. Without that pacing, the Cygnet ADC's native single-digit-µs throughput would finish the burst in well under one full mains cycle and produce RMS values that drift with whichever waveform fragment was captured. Three channels are read sequentially, so total active measurement time is approximately 1 second. The scale factor `CT_SCALE = (3.3/4096) × (2000/22)` converts ADC RMS counts directly to primary amps. Readings below 0.5 A are zeroed to suppress noise-floor pickup.
-
-**Temperature.** The Adafruit MCP9808 is initialized on every wake (since host power is fully cycled by `card.attn`), configured to 0.0625°C resolution, read once, and put to shutdown mode before the host sleeps. An absent or unresponsive sensor returns –999.0°C. The firmware tracks a separate `valid_temp_samples` counter that increments only when the reading passes the –40°C to +125°C sanity range; the summary average divides `sum_temp_c` by that counter rather than by total wake cycles. A window with zero valid temperature readings emits –999.0°C in the summary rather than a biased-low average. The Notecarrier CX's on-board I²C pull-ups are shared by the MCP9808 and the Notecard on the same bus — no external resistors are needed.
-
-### 7.5 Event payload design
-
-One [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefile for summaries (`xfmr_summary.qo`), one untemplated Notefile for alerts (`xfmr_alert.qo`). Templates trade flexibility for efficiency: the Notecard stores fixed-length binary records internally and the wire payload is ~3–5× smaller than free-form JSON, which matters across a fleet that may transmit daily summaries for a decade.
-
-Sample alert (phase imbalance event):
-```json
-{
- "file": "xfmr_alert.qo",
- "body": {
- "alert": "phase_imbalance",
- "i_a_rms": 91.4,
- "i_b_rms": 34.2,
- "i_c_rms": 88.7,
- "temp_c": 44.1,
- "extra": 62.6
- }
-}
-```
-The `extra` field carries alert-type-specific context: for `overload` it is the loading percentage of the worst phase; for `phase_imbalance` it is the computed imbalance percentage (max−min)/max; for `high_temp` it is the total current across all phases at the time of the alert.
-
-### 7.6 Low-power strategy
-
-The device is grid-tied (120VAC tap on the transformer secondary), so energy budget is not life-critical, but the sleep pattern matters anyway. Keeping the host off between samples eliminates a continuous 50–80mA baseline, reduces enclosure heat buildup, and makes the power profile immediately legible on a Mojo trace during bench validation.
-
-After each sample cycle the host calls `NotePayloadSaveAndSleep`, which serializes the `PersistState` struct to Notecard flash and issues [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) in sleep mode to cut host power for `sample_interval_sec` seconds. On the next wake, the Notecarrier re-powers the host; `NotePayloadRetrieveAfterSleep` pulls the saved struct back and execution continues where it left off. From the firmware author's perspective, the sleep call is a single function call; the Notecard manages the power rail.
-
-The Notecard Cell+WiFi itself idles at [~18 µA @ 5V](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) between cellular sessions. Alert Notes with `sync:true` bypass the outbound queue and wake the radio immediately; summary Notes accumulate in the on-device queue and are flushed together once per hour.
-
-### 7.7 Retry and error handling
-
-- The first Notecard transaction (`hub.set` on cold boot) uses `sendRequestWithRetry(req, 5)` to handle the cold-boot I²C race the `note-arduino` library documents.
-- Individual `env.get` calls return the default value if the Notecard returns NULL or an empty `text` field. Config values are then clamped to sane ranges (e.g., `sample_interval_sec` floored at 30 seconds) to guard against a misconfigured env var causing erratic behavior.
-- CT readings below the 0.5 A noise floor are zeroed; sample intervals where no phase exceeds the noise floor are excluded from current accumulation via the `valid_samples` counter, making the per-phase RMS averages *loaded-interval averages* rather than true time-weighted window averages. The `total_wakes` field in the summary payload records all host wakes (loaded + idle), so dividing `samples` by `total_wakes` gives the fraction of the window that carried detectable load — useful for utilization weighting (see [§11](#11-limitations-and-next-steps)). A summary window with zero valid CT samples still emits a `xfmr_summary.qo` Note with zero current fields and `samples: 0`, confirming the device is alive even when no load is detectable on the CTs.
-- Alert cooldown counters per alert type (default 6 cycles = 30 minutes at 5-min sample rate) prevent a sustained overload condition from paging the operations center every 5 minutes. Each alert type arms and disarms independently.
-- `fetchEnvOverrides` updates the `hub.set` outbound period if `summary_interval_min` has changed since the last successful fetch, keeping the Notecard's transmit timer synchronized with the firmware's summary window.
-
-### 7.8 Key code snippet 1: Note template definition
-
-Template fields use [numeric type hints](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates): `14.1` means 4-byte IEEE-754 float; `12` means 2-byte signed integer.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "xfmr_summary.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "i_a_rms", 14.1);
-JAddNumberToObject(body, "i_b_rms", 14.1);
-JAddNumberToObject(body, "i_c_rms", 14.1);
-JAddNumberToObject(body, "i_total", 14.1);
-JAddNumberToObject(body, "loading_pct", 14.1);
-JAddNumberToObject(body, "imbalance_pct", 14.1);
-JAddNumberToObject(body, "temp_c", 14.1);
-JAddNumberToObject(body, "overloads", 12);
-JAddNumberToObject(body, "samples", 12); // loaded intervals
-JAddNumberToObject(body, "total_wakes", 12); // all wakes (loaded + idle)
-notecard.sendRequest(req);
-```
-
-### 7.9 Key code snippet 2: CT RMS two-pass algorithm
-
-Pass 1 determines the actual DC bias offset (should be ~2048 counts for a well-built circuit; the measured value accounts for component tolerance). Pass 2 computes RMS of the centered AC signal using float accumulation to avoid 32-bit integer overflow at full-scale input. Both passes pace samples to a fixed 225 microseconds period so the RMS burst spans a deterministic ~333 milliseconds (≈20 mains cycles at 60 Hz) regardless of the host ADC's native throughput. See [§7.4](#74-sensor-reading-strategy) for why this matters.
-
-```cpp
-float readCtRms(uint8_t pin) {
- // Pass 1: establish DC offset (paced sampling — see §7.3).
- long sum = 0;
- uint32_t next_us = micros();
- for (int i = 0; i < CT_DC_SAMPLES; i++) {
- sum += analogRead(pin);
- next_us += CT_SAMPLE_PERIOD_US;
- int32_t wait = (int32_t)(next_us - micros());
- if (wait > 0 && wait < (int32_t)CT_SAMPLE_PERIOD_US) {
- delayMicroseconds((uint32_t)wait);
- }
- }
- int offset = (int)(sum / CT_DC_SAMPLES);
-
- // Pass 2: AC RMS over ~333 ms (≈20 mains cycles at 60 Hz).
- float sum_sq = 0.0f;
- next_us = micros();
- for (int i = 0; i < CT_RMS_SAMPLES; i++) {
- float s = (float)(analogRead(pin) - offset);
- sum_sq += s * s;
- next_us += CT_SAMPLE_PERIOD_US;
- int32_t wait = (int32_t)(next_us - micros());
- if (wait > 0 && wait < (int32_t)CT_SAMPLE_PERIOD_US) {
- delayMicroseconds((uint32_t)wait);
- }
- }
- float i_rms = sqrtf(sum_sq / CT_RMS_SAMPLES) * CT_SCALE;
- return (i_rms < CT_NOISE_FLOOR_A) ? 0.0f : i_rms; // zero noise floor
-}
-```
-
-### 7.10 Key code snippet 3: immediate-sync alert
-
-`sync:true` tells the Notecard to skip the outbound queue timer and open a cellular session immediately. On cellular the alert typically reaches Notehub within 15–60 seconds of the threshold trip.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "xfmr_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "phase_imbalance");
-JAddNumberToObject(body, "i_a_rms", i_a);
-JAddNumberToObject(body, "i_b_rms", i_b);
-JAddNumberToObject(body, "i_c_rms", i_c);
-JAddNumberToObject(body, "temp_c", temp_c);
-JAddNumberToObject(body, "extra", imbalance_pct);
-notecard.sendRequest(req);
-```
-
-### 7.11 Key code snippet 4: sleep and state persistence
-
-After sending any queued Notes, the host serializes its runtime state to the Notecard and cuts its own power for the next sample interval. On the next ATTN wake, `NotePayloadRetrieveAfterSleep` restores the struct; execution re-enters `setup()` from the top.
-
-```cpp
-NotePayloadDesc out = {0, 0, 0};
-NotePayloadAddSegment(&out, SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&out, cfg.sample_interval_sec, NULL);
-// Execution does not continue past here under normal operation.
-```
-
-
-## 8. Data Flow
-
-
-
-Every 5 minutes (`sample_interval_sec`), the host wakes, reads the configured CT channels (A0 always; A1 when `phase_count≥2`; A2 when `phase_count=3`) and the temperature sensor, and evaluates three threshold rules against the freshly-read values and accumulated history.
-
-**Collected.** Per-phase RMS current (A) on phases A, B, and C; enclosure temperature (°C); total CT current (sum of phases); derived loading percentage; derived phase-imbalance percentage; count of overloaded sample intervals in the current summary window.
-
-**Transmitted:**
-- `xfmr_summary.qo` — one record per `summary_interval_min` (default 24 records/day), containing per-phase means over *loaded* sample intervals (intervals in which at least one phase exceeded the 0.5 A noise floor), mean enclosure temperature, count of overloaded sample intervals, number of loaded samples (`samples`), and total host wakes in the window (`total_wakes`). Template-encoded; queued and flushed by the Notecard's hourly outbound sync. Multiply `loading_pct` by `samples / total_wakes` to recover the true time-weighted window average (see [§11](#11-limitations-and-next-steps)).
-- `xfmr_alert.qo` — emitted immediately on threshold trip, with `sync:true` to bypass the outbound interval. Each alert type has a 30-minute cooldown to prevent alert storms.
-
-**Triggers.** Three independent rules:
-- **`overload`** — any single phase exceeds `rated_amps × (overload_pct / 100)`. The `extra` field carries the peak phase's loading percentage. A transformer persistently above 95% of nameplate rating faces insulation aging that shortens its service life measurably.
-- **`phase_imbalance`** — `(max_phase − min_phase) / max_phase` exceeds `imbalance_pct_thresh` while at least one phase is above 2A. The imbalance check is skipped entirely when `phase_count=1` (single-leg mode); `imbalance_pct` is always 0.0 in both the real-time alert path and the summary average. With `phase_count=2`, only L1 and L2 are compared — the unconnected C channel (always 0A) is excluded from the min/max calculation. The `extra` field carries the computed imbalance percentage. On three-phase transformers, phase imbalance generates negative-sequence currents that heat the core, waste capacity, and can push a lightly-loaded phase into overvoltage.
-- **`high_temp`** — enclosure temperature exceeds `temp_alert_c`. The `extra` field carries the total current across all phases at the time of the alert. Temperature combined with load is the primary mechanism of accelerated transformer aging; the Arrhenius rule of thumb says every 10°C above design temperature halves insulation life.
-
-**Routed.** Both Notefiles flow through Notehub. From there, operators configure routes to send `xfmr_alert.qo` to an outage-management system or on-call paging tool and `xfmr_summary.qo` to a long-term historian for trending and transformer-life analytics.
-
-
-## 9. Validation and Testing
-
-**Expected steady-state behavior.** On a healthy, lightly-loaded transformer with well-balanced phases, the **Events** tab in Notehub should show one `xfmr_summary.qo` per hour and zero `xfmr_alert.qo` events. The serial monitor should print `[sample]` lines every 5 minutes and `[summary] queued` once per hour (the Note reaches Notehub on the next outbound sync).
-
-**Bench bring-up without a live transformer.** Connect the CTs to a known-current AC circuit (a 120VAC lamp load on one phase works well) and verify the `i_a_rms` field in the summary matches the expected current (within ~5%). With no load on a CT channel, the reading should be 0.0A — if it's non-zero, the bias network has a wiring error or the ADC offset is drifting outside the noise floor.
-
-**Triggering alerts for validation.** Drop `overload_pct` to `50.0` in the Fleet environment (the minimum the firmware permits. See §7.6 for the full clamp range). The next inbound sync (≤2 hours by default) will pull the new value, and the next sample will fire an `overload` alert on any load above half of `rated_amps`. Reset `overload_pct` to your intended value afterward.
-
-**Power validation with Mojo.** The [Notecard Cell+WiFi (MBGLW) idle current is ~18 µA @ 5V](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/). Cellular transmit sessions draw ~250 mA average with brief peaks up to ~2 A. The host MCU, fully gated off by `card.attn`, contributes essentially nothing between samples.
-
-Expected power profile at the defaults (5-min sample, hourly transmit):
-
-| Firmware state | Expected current |
-|---|---|
-| Notecard idle between samples (radio off) | ~18 µA — Notecard IC per datasheet; the Mojo reading for the full board at 5V with the host off reflects additional board quiescent draws. Use the Mojo trace, not the 18 µA figure, for whole-assembly power budgeting. |
-| Host active: ADC sampling (~1 seconds per wake, 3 channels × ~20 mains cycles each) | Measure with Mojo on your actual assembly — Cygnet and Notecarrier board current during ADC sampling is assembly-specific and is not a published Notecard figure. |
-| Cellular session (hourly, ~10–30 seconds) | ~250 mA avg, ~2 A peak @ 5V |
-| 24-hour session energy (from published Notecard figures) | Cellular sessions: 24 sessions × 10–30 seconds × 250 mA avg ÷ 3600 ≈ **17–50 mAh/day**. Host ADC wakes and board/regulator quiescent current are assembly-specific — measure both with Mojo on your actual hardware and add them to the cellular figure to establish the whole-assembly daily budget before field deployment. |
-
-A useful Mojo trace to look for:
-- **Healthy:** a sustained low baseline at the board's quiescent current level (establish the exact value with Mojo during commissioning, it reflects Notecarrier and regulator draws with the host off), brief ~1 seconds blips every 5 minutes (host ADC reads across three channels), one 10–30 second burst at ~250 mA per hour (cellular sync).
-- **Host not sleeping:** a sustained elevated baseline significantly above the quiescent level you measured with the host off. Almost always a `card.attn` wiring issue or `NotePayloadSaveAndSleep` returning early.
-- **Weak signal:** correctly-spaced hourly bursts but each burst is >60 seconds at peak. Route the cellular antenna away from the transformer casing.
-
-Mojo is a bench-validation and regression tool — it is not required in production. Once a firmware revision passes the trace check, deployed units don't need it.
-
-
-## 10. Troubleshooting
-
-| Symptom | Cause | Solution |
-|---|---|---|
-| Serial console shows `[sample]` lines but no `[summary]` lines after 60+ minutes | Summary not being queued | Check that `summary_interval_min` env var is set (default 60). If you see `[summary] queued` but no Notehub events, the Notecard is not syncing; see "No events in Notehub" row below. |
-| No events appearing in Notehub **Events** tab after 2+ hours | Device not syncing to Notehub | Verify the Notecard has a valid SIM (Blues prepaid or user-provided). Check Notecard's cellular signal with `card.signal` request in Notehub **Devices → Terminal**. If signal is poor, relocate the antenna away from the transformer core or switch to an outdoor SMA antenna (see §5). |
-| Mojo trace shows sustained elevated baseline (not just brief ~1 seconds spikes every 5 minutes) | Host not sleeping between samples | Verify `card.attn` wiring is correct on the Notecarrier CX (see §5). If `NotePayloadSaveAndSleep()` is not working, check for exceptions in the serial log. Most common cause: `PRODUCT_UID` is not set, causing the firmware to return early before calling sleep. |
-| Current readings are non-zero even with no load on CTs | Bias circuit error or ADC offset drift | Verify all three bias circuits are correctly wired (see §5 diagram). Check that the capacitor's positive plate points toward the ADC pin. If wiring is correct, the AD offset may be outside the 0.5 A noise floor; reduce the noise floor temporarily by modifying `CT_NOISE_FLOOR_A` in the firmware for debugging. |
-| CT readings are consistently wrong (e.g., reading 50 A when the load is 100 A) | Incorrect `CT_SCALE` constant or ADC resolution mismatch | Verify `analogReadResolution(12)` is called before any analogRead (it is, in setup()). If a different ADC resolution was selected before flashing, the scale factor is off by a factor of (new_bits / 12). Recalculate `CT_SCALE = (3.3 / 4096) × (2000 / 22)` for 12-bit resolution; see §7.3 for the formula. |
-| Mojo shows correct steady state but summary payload has temp_c = -999.0 | MCP9808 not responding | Verify I²C wiring (SDA/SCL). Check I²C address is 0x18 (default). Try the Adafruit_MCP9808 example sketch to confirm the sensor responds. |
-
-
-## 11. Limitations and Next Steps
-
-A pole-mount monitor that drives operational decisions about live distribution equipment has a high bar to meet in production: surge protection, signed utility approvals, billing-grade accuracy where it matters, and survivable comms when the transformer itself fails. This POC keeps the implementation narrow on purpose, with three signals (per-phase load, imbalance, enclosure thermal stress) over one cellular path, so the alerting and trending pieces stay readable. The items below mark both where that scope was drawn and the natural production additions.
-
-### Simplified for this POC
-
-Each of these is a deliberate scope boundary that keeps the alerting and trending pieces readable, paired with what a production build would change.
-
-**The CT current range is bounded by the chosen sensor.** The SCT-013-000 (100A) suits transformers up to approximately 20 kVA at 240V (~83A full-load). Transformers at or above 25 kVA at 240V draw ~104A, beyond this CT's rating, and require a higher-range **current-output** CT (200A or 400A). **Voltage-output CTs are not drop-in replacements**; see §4. The firmware `CT_TURNS_RATIO` and `CT_BURDEN_OHMS` constants need to be updated to match the chosen CT; all downstream calculations scale automatically.
-
-**RMS accuracy is monitor-grade, not billing-grade.** The two-pass ADC algorithm covers approximately 20 mains cycles per channel (~330 milliseconds per channel, ~1 second total for three channels) at the Cygnet ADC's throughput rate. This gives adequate accuracy (~5–10%) for a load-threshold monitor, but is **not suitable for billing-grade energy metering**. Extending the sample count to cover ~200 mains cycles per channel would improve RMS accuracy at the cost of proportionally longer host-active time.
-
-**Summary averages reflect loaded intervals only.** `i_a_rms`, `i_b_rms`, `i_c_rms`, `i_total`, `loading_pct`, and `imbalance_pct` in `xfmr_summary.qo` are computed only over the sample intervals (`samples`) where at least one phase exceeded the 0.5 A noise floor. Sample intervals where no load is detectable are excluded from the denominator. This means a lightly-loaded or intermittently-loaded window reports average current *during the intervals when load was present*, not the true time-weighted average across the full window. For fault detection and threshold alerting this is the appropriate behavior — alert thresholds should be evaluated against actual load conditions, not diluted by idle time. For utilization reporting or transformer-life analytics, multiply `loading_pct` by `samples / total_wakes` (both fields are in the summary payload) to recover the time-weighted window average.
-
-**Power factor is not measured.** The firmware reports apparent current (A RMS), not real power (watts) or reactive power (VAR). A transformer nameplate rating is in kVA (apparent power), so loading percentage is correct as stated. Measuring true power would require voltage sensing in addition to current sensing — a meaningful production enhancement for a billing or power-quality application.
-
-**Temperature is single-point, an enclosure proxy and not a true ambient probe.** This implementation uses an MCP9808 mounted inside the enclosure. It measures internal enclosure temperature, not true outdoor ambient air temperature and not transformer core or winding temperature. Enclosure temperature tracks thermal stress directionally but can diverge significantly from outdoor ambient, especially on a sun-exposed pole, and lags winding temperature by minutes to hours under transient load changes. A true ambient probe (external housing, shielded from direct sun) or a winding-contact sensor (PT100 RTD or thermocouple on the transformer case) would provide more actionable data; see Production Next Steps below.
-
-**This design uses conventional split-core CTs, not Rogowski coils.** This implementation uses conventional current-output split-core CTs (YHDC SCT-013-000) rather than flexible Rogowski-type coils. True Rogowski coils produce a signal proportional to d*i*/d*t*, requiring an external integrator board before the ADC. They offer no inherent accuracy advantage for power-frequency monitoring over calibrated current-output CTs and add meaningful cost and complexity. Flexible Rogowski coils (such as the Dent Instruments PowerScout or Magnelab RCT series) are the right choice for physically constrained installations where a rigid split-core jaw cannot open wide enough to encircle a large busbar or conductor bundle. That constraint does not apply to standard residential or commercial distribution transformer secondary leads, so this design uses the simpler current-output CT.
-
-**There is no inductive power harvest.** The project description mentions optional inductive energy harvesting from the transformer. It is omitted from this POC because the complexity and installation constraints of a validated inductive harvester are substantial relative to the simplicity of a direct 120VAC tap on the transformer secondary. For installations where tapping the secondary is impractical (sealed enclosures, third-party owned equipment), inductive harvesting is the right production next step, but it would require a separate, validated power-stage design and is out of scope here.
-
-**There is no backup power.** If the transformer fails (the scenario we're trying to detect early), the monitoring device also loses power and cannot report the outage itself. Adding a Blues Scoop with a small LiPo battery would allow the device to survive a brief outage and transmit a "power lost" event before the battery dies, a meaningful production addition for "last gasp" outage detection.
-
-**Alert cooldown is cycle-based.** The cooldown counter tracks sample cycles, not wall-clock time. If `sample_interval_sec` is changed via env var after an alert fires, the cooldown duration shifts proportionally. For most deployments this is acceptable; for strict SLA compliance, store the alert timestamp in persistent state instead.
-
-**There is no surge or transient protection.** The 2A slow-blow fuse on the 120VAC tap protects against a sustained overcurrent fault but provides no suppression of lightning-induced transients, switching surges, or capacitor-bank switching events — all of which are common on distribution transformer secondaries. A permanent pole-mount deployment requires: a surge protective device (SPD) rated for the site's overvoltage category (Category C / lightning-level, per IEC 61643-11 or UL 1449) installed on the Line/Neutral entry ahead of the fuse; enclosure grounding and bonding to the pole ground system per applicable NEC sections and utility rules; verified creepage and clearance in all mains-voltage wiring and connectors; UV and temperature qualification of all external cabling and the enclosure; and written utility approval before the secondary tap is permanently energized. This POC omits the SPD and grounding circuit as out of scope for a bench or controlled field-trial context; **do not omit them in any design intended for permanent field deployment.**
-
-### Production Next Steps
-
-These additions take the monitor from a controlled trial toward a permanently energized, fleet-scale pole-mount deployment.
-
-**Surge and grounding protection** is mandatory before permanent field deployment: a surge protective device (SPD) on the AC mains entry (Category C rated, per IEC 61643-11 or UL 1449), enclosure grounding/bonding to the pole ground system per applicable NEC sections and utility rules, and written utility approval.
-
-**Higher-range CTs** (200A, 400A) cover 50–167 kVA distribution transformers; update `CT_TURNS_RATIO` and `CT_BURDEN_OHMS` accordingly.
-
-**A contact temperature probe** on the transformer case enables direct winding-temperature monitoring — a PT100 RTD via MAX31865 (SPI, requires three additional GPIO pins for SCLK/MISO/CS) or a thermocouple with a MAX31856 (also SPI). An I²C RTD option such as the Texas Instruments TMP117 (±0.1°C, I²C) avoids the extra SPI pins but is limited to –40°C to +125°C, which covers typical transformer enclosure ranges.
-
-**Voltage measurement on the secondary** (a resistor divider plus a precision analog input or an external ADC) would let the device compute real power, power factor, and true apparent kVA loading.
-
-**Inductive energy harvesting** from a current-carrying secondary conductor is an alternative to the 120VAC tap — valuable for sealed enclosures where the tap is not accessible.
-
-**A Blues Scoop with LiPo** enables last-gasp outage reporting when the primary supply fails.
-
-**Over-the-air host firmware updates** across the deployed fleet are critical when physical re-flash of hundreds of pole-mount units is impractical. [Notecard Outboard Firmware Update (ODFU)](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) provides this capability; verify the specific wiring and bootloader support for the Notecarrier CX / Cygnet combination against Blues documentation before committing to a production design.
-
-**Per-transformer commissioning** would add a one-time `rated_amps` calibration via env var at install time to account for field-measurement of the actual secondary current at known load.
-
-
-## 12. Summary
-
-The pole-top transformer that used to be invisible until Friday's peak demand took down the neighborhood now reports for itself every hour. Three split-core CTs clamped on the secondary leads plus one I²C temperature sensor give the operations center the three signals that matter most for transformer health (per-phase loading, phase imbalance, and thermal stress) with hourly summaries for capacity planning and immediate `xfmr_alert.qo` Notes when a phase climbs past 95% of rated current or the enclosure runs hot. The same SKU, the same SIM, and the same firmware image work on a dense urban feeder and a rural single-wire run, which is the difference between a pilot that works on paper and a fleet program that actually scales. The dispatcher gets the heads-up before the streetlights go out; the planner gets a year of loading history without a single truck roll to read it.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/utility-distribution-transformer-load-monitor/)**
diff --git a/79-utility-distribution-transformer-load-monitor/banner.png b/79-utility-distribution-transformer-load-monitor/banner.png
deleted file mode 100644
index 959035c8..00000000
Binary files a/79-utility-distribution-transformer-load-monitor/banner.png and /dev/null differ
diff --git a/79-utility-distribution-transformer-load-monitor/diagrams/01-system-architecture.svg b/79-utility-distribution-transformer-load-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index 92dbd88a..00000000
--- a/79-utility-distribution-transformer-load-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/79-utility-distribution-transformer-load-monitor/diagrams/02-wiring-assembly.svg b/79-utility-distribution-transformer-load-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 63d271c3..00000000
--- a/79-utility-distribution-transformer-load-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,75 +0,0 @@
-
diff --git a/79-utility-distribution-transformer-load-monitor/diagrams/03-data-flow.svg b/79-utility-distribution-transformer-load-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index 2f9b51c3..00000000
--- a/79-utility-distribution-transformer-load-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
diff --git a/80-ev-charger-session-utilization-monitor/README.md b/80-ev-charger-session-utilization-monitor/README.md
index 857ffa5b..17cc09cd 100644
--- a/80-ev-charger-session-utilization-monitor/README.md
+++ b/80-ev-charger-session-utilization-monitor/README.md
@@ -1,470 +1,5 @@
-# Level 2 EV Charger Session & Utilization Monitor
+# EV Charger Session & Utilization Monitor
-
-
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Level%202%20EV%20Charger%20Session%20%26%20Utilization%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/ev_charger_session_monitor/](firmware/ev_charger_session_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a cellular [energy monitoring](https://blues.com/solutions-energy-monitoring/) retrofit for Level 2 EV chargers: for fleet managers, facilities teams, and charging-network operators who need to know how their installed chargers are actually being used. The design clamps a DIN-rail energy meter onto each charger's AC feed and reports three streams to the [Blues Notehub](https://blues.com/notehub/) cloud service over cellular: per-session metered kWh and peak power, hourly utilization and availability, and mains-offline alerts. Energy is measured by a hardware-metered instrument; charger availability is tracked from the meter's V_rms register — when mains voltage is absent the circuit is definitively offline. No modification to the charger hardware, no OCPP enrollment, no site IT involvement required. The hardware is a Notecarrier CX with a Notecard Cell+WiFi and an EASTRON SDM120-Modbus energy meter (see §4 for the BOM). See [§10](#10-limitations-and-next-steps) for design boundaries and production expansion paths.
-
-## 1. Project Overview
-
-
-**The problem.** Fleet managers, facilities teams, and third-party charging network operators are installing Level 2 **EVSE** (electric vehicle supply equipment) at workplace parking lots, fleet depots, and retail locations, and almost universally, they have no idea how those chargers are actually used. Is the 48-amp circuit in Bay 3 running at 90% utilization while Bay 4 sits idle all day? Are sessions clustering in the morning rush so that employees arriving at 9 a.m. find every port occupied? Without instrumentation, all of these questions get answered by anecdote or not at all.
-
-This project addresses the instrumentation gap directly: insert a series-wired DIN-rail energy meter (EASTRON SDM120-Modbus) on the EVSE circuit feed, poll it over Modbus RTU, and report session-level energy delivery and port utilization directly — independent of who built the charger, what network it's connected to, and whether the site team has any standing with the charging network operator. OCPP-based integrations provide richer per-driver and per-RFID records where a charger exposes that interface, but they depend on network-operator cooperation and vary across Blink, ChargePoint, EV Connect, and in-house fleet stacks. The hardware-meter approach requires none of that: it reads the physics of the circuit, and that works on any Level 2 EVSE ever installed.
-
-**What this design delivers.** A **metered session and utilization monitor** built around a DIN-rail single-phase energy meter (EASTRON SDM120-Modbus or compatible) on the EVSE feed. The meter is polled every 30 seconds over Modbus RTU; per-session energy is the difference in the meter's cumulative import-kWh register between session open and close — hardware-computed, not estimated from current alone. Peak power (W) is the highest active-power reading seen during the session. Charger availability is derived from the meter's V_rms register: the firmware tracks how many minutes per reporting window the supply voltage was above the configurable `voltage_present_v` threshold (`available_min`) and divides that by the full wall-clock duration of the window (`elapsed_min`) to produce `availability_pct`. Wakes where the meter is unreachable — because mains is absent, wiring is faulted, or all Modbus retries fail — count against availability in the denominator, so the metric correctly falls below 100 % whenever the charger was offline for any reason. A companion `sample_coverage_pct` field flags the fraction of window time that had valid meter readings, giving a data-quality signal separate from the availability signal. A separate `mains_absent` alert fires when no mains-present reading has been seen for longer than the configurable `alert_offline_min` threshold: a definitive signal for a tripped breaker, power loss, or sustained meter fault. Sites that additionally need per-driver or per-RFID records should implement an OCPP back-end integration alongside this meter-based monitor. See [§10](#10-limitations-and-next-steps) for boundaries and expansion paths.
-
-**Why Notecard.** The single biggest deployment friction for EV charger instrumentation is the network question. Chargers installed in a corporate parking structure are almost always on a circuit run by a third-party operator (a fleet management company, a charging network, or a facilities contractor) whose people have zero standing with the site's IT department. The parking lot is treated as a hostile network zone: no WiFi credentials are available, no VLAN is provisioned, and no IT ticket is going to get approved by Friday. Even for in-house fleet teams, the "just connect it to the corporate network" path involves months of security review for every new device class.
-
-
-
-Cellular sidesteps all of that. A Notecard Cell+WiFi (MBGLW) with its included prepaid SIM installs in the same electrical panel as the charger circuit, connects to an LTE Cat-1 bis tower in the parking lot, and is reporting session data within minutes — with no site IT involvement, no network form, no AP to pair to. WiFi remains as an opportunistic fallback for the rare site that has a legitimate parking-structure AP, but it is never required. The same firmware image and the same hardware SKU deploy identically in a workplace, a fleet depot, and a multi-tenant retail center.
-
-**Deployment scenario.** A DIN-rail assembly mounted inside the electrical panel that feeds the EVSE circuit. The SDM120-Modbus energy meter is wired in series on the EVSE circuit (live conductor through the meter's current input); a SparkFun BOB-10124 (SP3485) RS-485 transceiver breakout bridges the meter's RS-485 port to the Cygnet's 3.3 V UART. The Notecarrier CX and Notecard MBGLW are powered from a 5 V / 3 A DIN-rail supply (e.g. MeanWell HDR-15-5) fed from whatever mains voltage is present at the panel, typically 120 VAC line-to-neutral where a neutral is accessible at the subpanel, or 208/240 VAC line-to-line where only two line conductors are available. The HDR-15-5 accepts 85–264 VAC and handles both. Total panel footprint is four to six DIN-rail modules. No modification to the charger hardware, no OCPP enrollment, no IT coordination required.
-
-**Panel placement Note.** Whether the DIN-rail assembly can be mounted inside the main EVSE panel depends on the enclosure's listing and local electrical code, not all listed enclosures permit third-party low-voltage auxiliary equipment in the mains compartment. Where the panel interior is not available, mount the assembly in a separately listed auxiliary enclosure bolted adjacent to the main panel and route the RS-485 signal cable and AC supply conductors between the two enclosures through a listed conduit entry or knockout. The auxiliary-enclosure approach is the safe default for any installation where the main panel's suitability cannot be confirmed.
-
-## 2. System Architecture
-
-
-
-
-**Device-side responsibilities.** The work in the panel runs on a 30-second loop. The onboard Cygnet STM32L4 on the Notecarrier CX wakes via [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn), polls the SDM120 over Modbus RTU for V_rms, active power, and import kWh, and runs a two-state session machine: is the charger actively delivering energy or not? Per-session energy is the difference in the meter's cumulative import-kWh register between session open and close, so the firmware never has to estimate from current alone or assume a power factor. Availability is tracked the same way each wake — `window_elapsed_sec` advances unconditionally (wall-clock time) while `window_available_sec` only advances when the meter is valid and V_rms ≥ `voltage_present_v`. The resulting `availability_pct` correctly penalizes any wake where the circuit was offline or unreadable, instead of hiding it. When a session closes, the host queues a `charger_session.qo` Note immediately with `sync:true`; window statistics queue into `charger_summary.qo` on the report cadence. State persists to Notecard flash between wakes through `NotePayloadSaveAndSleep`, and the host typically stays awake less than two seconds per wake.
-
-**Notecard responsibilities.** The Notecard owns the network. It queues [Notes](https://dev.blues.io/api-reference/glossary/#note) locally, establishes the cellular (or WiFi) session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes), and immediately flushes any `sync:true` session or alert Note the moment it arrives. It also distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from Notehub, so an operator can retune thresholds and reporting intervals across a fleet of panels without ever re-flashing firmware.
-
-**Notehub responsibilities.** Once Notes leave the panel, the Notecard's embedded global SIM carries them over supported carriers worldwide and lands them in [Notehub](https://notehub.io). Three Notefiles arrive separately so they can take different paths downstream: `charger_session.qo` (per completed session, real-time) to a utilization dashboard or fleet management system; `charger_summary.qo` (periodic) to a long-term analytics store; and `charger_alert.qo` (mains-absent / charger-offline alert) to an on-call endpoint. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) give you the natural organization layer: one fleet per property keeps environment-variable overrides (a site's known line voltage, say) scoped correctly.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations. Route setup is project-specific and outside the scope of this reference. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for detailed configuration.
-
-
-## 3. Technical Summary
-
-
-Before diving into the full documentation, here's the fastest path from parts to first event in Notehub:
-
-1. **Create a Notehub project** at [notehub.io](https://notehub.io) and copy its ProductUID (looks like `com.your-company:ev-charger-monitor`).
-2. **Wire the bench rig** — Notecarrier CX + Notecard MBGLW + SDM120-Modbus energy meter + SparkFun BOB-10124 RS-485 transceiver (D0/D1/D2 pins). Full wiring details in [§5](#5-wiring-and-assembly).
-3. **Edit firmware/ev_charger_session_monitor/ev_charger_session_monitor.ino** — replace the empty string on line 53 (`#define PRODUCT_UID ""`) with your ProductUID.
-4. **Flash with Arduino IDE** — open the sketch, select **Blues Cygnet** as the board (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`), hit **Upload**. Or use `arduino-cli` (see [§7.1](#71-installing-and-flashing) for commands).
-5. **Watch Notehub** — open your project's **Events** tab. You'll see `_session.qo` within seconds (proves radio works), a `charger_summary.qo` within the hour, and a `charger_session.qo` after you run a test load (see [§9](#9-validation-and-testing)).
-
-For detailed component selection, wiring, Notehub configuration, and validation, read on.
-
-Here is a sample Note this device emits:
-
-```json
-{
- "sessions": 1,
- "total_kwh": 5.9,
- "avg_session_kwh": 5.9,
- "peak_w": 10320.0,
- "charging_min": 38,
- "idle_min": 22,
- "utilization_pct": 63.3,
- "available_min": 60,
- "availability_pct": 100.0,
- "sample_coverage_pct": 100.0
-}
-```
-
-
-## 4. Hardware Requirements
-
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an onboard Cygnet STM32L4 host — no separate MCU needed. The ATTN pin wiring enables the Notecard to cut host power between samples for minimum idle draw. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | LTE Cat-1 bis with global coverage and a WiFi fallback. Cellular is the primary path; see §1 for why site WiFi is rarely a viable option in EV charger deployments. Includes 500 MB data and 10 years of service on the included SIM. For technical details, see the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/). |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(bench validation only)* | 1 | Coulomb counter for measuring per-wake energy and validating the sleep/transmit power profile during bring-up. Not deployed to the field. See [§9](#9-validation-and-testing). |
-| [SparkFun CEL-16432 LTE Hinged External Antenna](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html), 698–2700 MHz, SMA Male | 1 | Required for reliable signal inside a metal enclosure. The SMA connector screws onto the SMA female pigtail at the panel wall, placing the radiating element outside the metal cabinet. A rubber-duck antenna left inside a steel panel will degrade signal significantly. |
-| [SparkFun WRL-18568 SMA to U.FL Cable, 150 mm](https://www.sparkfun.com/products/18568) | 1 | Routes the Notecard's U.FL cellular port to the SMA female bulkhead connector in the panel wall. For panel layouts where the Notecarrier mounts farther from the wall, a longer SMA-to-U.FL pigtail from Mouser or DigiKey can substitute. |
-| [EASTRON SDM120-Modbus 45 A direct-connect single-phase DIN-rail energy meter](https://www.eastroneurope.com/products/view/sdm120modbus) | 1 | Wired in series on the EVSE circuit (live conductor through the current input terminals). Measures V_rms, active power (W), and cumulative import kWh with Class 1 accuracy. RS-485 Modbus RTU interface connects to the Cygnet UART. Rated 45 A direct-connect, covering 32 A and 40 A Level 2 EVSE; for 48 A or 60 A EVSE circuits use the SDM120CT variant with an appropriate 5 A secondary split-core CT, or the SDM230-Modbus (100 A direct). The SDM120 requires both a live conductor and a neutral terminal for voltage sensing — confirm both are accessible at the panel before specifying this meter. The Modbus protocol document (register map, baud-rate defaults, function codes) is available as a PDF download after free registration on the [Eastron Europe website](https://www.eastroneurope.com/products/view/sdm120modbus). |
-| [SparkFun RS-485 Transceiver Breakout – SP3485 (BOB-10124)](https://www.sparkfun.com/products/10124) | 1 | Converts the Cygnet's 3.3 V UART to the RS-485 differential pair required by the SDM120. The SP3485 chip operates from a single 3.3 V supply, eliminating logic-level compatibility concerns that arise with 5 V-only transceivers. Wire RO to D0 (Serial1 RX), DI to D1 (TX), and the RTS pad to D2; the board routes the RTS pad to both DE (driver enable) and RE (receiver enable) internally, so no separate jumper is needed. VCC to +3V3, GND to GND. |
-| AC/DC DIN-rail supply, 85–264VAC input, 5 V / 2.4 A output (e.g. [MeanWell HDR-15-5](https://www.meanwell.com/Upload/PDF/HDR-15/HDR-15-SPEC.PDF)) | 1 | Derives 5 V DC from whatever mains voltage is present at the panel (120 VAC L-N or 208/240 VAC L-L). The 2.4 A output rating provides adequate transient headroom for Notecard cellular transmit bursts (≤2 A peak). DIN-rail mount integrates cleanly into the panel enclosure alongside the EVSE breaker. |
-| DIN-rail enclosure, ≥6-module width | 1 | Houses the Notecarrier CX, DIN-rail supply, and RS-485 transceiver module inside the electrical panel. The SDM120 itself is a separate DIN-rail device wired in series on the EVSE circuit. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-
-
-
-**Electrical safety.** The 120/240VAC feed inside an electrical panel is hazardous. This installation inserts the SDM120 energy meter **in series** on the EVSE feed, which requires interrupting and re-terminating the live mains conductors. Panel work **must be performed by a qualified electrician** following applicable electrical codes and lockout/tagout procedures.
-
-
-
-
-
-**Panel placement.** Mounting third-party low-voltage electronics inside a mains EVSE panel is subject to the enclosure's UL/CSA listing and local electrical code. Where the panel listing or jurisdiction does not permit auxiliary equipment in the mains compartment, install the Notecarrier CX, power supply, and RS-485 transceiver module in a separately listed auxiliary enclosure mounted adjacent to the main panel. Route the RS-485 signal cable and AC supply conductors between the two enclosures through a listed conduit entry or knockout. The auxiliary-enclosure approach is the safe default for any installation where the main panel's interior cannot be confirmed suitable.
-
-
-
-**SDM120 meter installation.** The SDM120-Modbus is a series element: the EVSE circuit live conductor must pass through the meter's current input terminal. Break the live conductor feeding the EVSE and route it through the meter's current input terminals — line-in on the input terminal and line-out on the output terminal. **Follow the terminal labels printed on the SDM120 faceplate exactly**; designations vary by variant and firmware revision, so do not rely on a generic reference for this wiring. Connect the neutral conductor to the meter's neutral terminal — the SDM120 requires a neutral connection for its internal voltage measurement. Do **not** route the neutral through the current input. The SDM120 derives its own supply from the same line and neutral conductors feeding the EVSE; consult the datasheet for the exact supply terminal connections on your variant. This installation interrupts the EVSE circuit during wiring; follow lockout/tagout procedures and have a qualified electrician perform this work.
-
-
-
-**Circuit ampacity.** The SDM120-Modbus is rated 45 A continuous. It is suitable for 32 A EVSE circuits (40 A breaker) and 40 A EVSE circuits (50 A breaker). For 48 A EVSE (60 A breaker) or higher, substitute the SDM120CT variant with an appropriately rated split-core CT, or the SDM230-Modbus (100 A rated). See [§10](#10-limitations-and-next-steps) for substitution guidance.
-
-
-
-**RS-485 wiring.** Connect the SDM120's RS-485 terminals (**A+** and **B−**) to the corresponding **A** and **B** terminals on the SparkFun BOB-10124 breakout (via the 3.5 mm screw terminal or 0.1" header) using a twisted-pair cable (120 Ω characteristic impedance recommended). Wire the breakout to the Notecarrier CX header:
-
-- **BOB-10124 RO** (receiver output) → **D0** (Serial1 RX) on the Notecarrier CX dual 16-pin header
-- **BOB-10124 DI** (driver input) → **D1** (Serial1 TX)
-- **BOB-10124 RTS** (direction control) → **D2** — the board routes this pad to both DE and RE internally
-- **BOB-10124 VCC** → **+3V3**
-- **BOB-10124 GND** → **GND**
-
-The firmware drives D2 HIGH before transmitting a Modbus request frame and LOW immediately after, switching the transceiver from receive to transmit and back. A 120 Ω termination resistor across the A+ and B− terminals at the far end of the RS-485 bus reduces reflections; at the short cable lengths typical in a panel enclosure (< 1 m) it is optional.
-
-**Notecard and antenna.** Seat the Notecard Cell+WiFi (MBGLW) into the Notecarrier CX's M.2 slot. Connect the u.FL to SMA pigtail to the Notecard's cellular u.FL port; thread the SMA end through a cable gland in the panel enclosure wall and attach the external antenna outside the panel. A rubber-duck antenna left inside a metal panel will degrade signal significantly. The Notecard communicates with the Cygnet host over I²C on the internal Notecarrier bus; no additional wiring is needed for that path.
-
-**Power.** Wire mains power to the MeanWell HDR-15-5 input terminals — 120 VAC line-to-neutral where a neutral conductor is accessible at the panel, or 208/240 VAC line-to-line where only two line conductors are available; the HDR-15-5 accepts 85–264 VAC and handles both configurations. Consult local electrical code for the permitted supply tap. Connect the HDR-15-5's 5V and GND output terminals to the Notecarrier CX's `+VBAT` and `GND` pads. For bench bring-up, insert the **Mojo** in series between the HDR-15-5's 5V output and the `+VBAT` pad, and connect the Mojo's Qwiic port to the Notecarrier CX's Qwiic connector — see [§9](#9-validation-and-testing) for what to look for on the trace.
-
-Pin summary:
-- **D0** (Serial1 RX) → BOB-10124 RO
-- **D1** (Serial1 TX) → BOB-10124 DI
-- **D2** → BOB-10124 RTS (routes to DE + RE internally)
-- **+3V3** → BOB-10124 VCC
-- **GND** → BOB-10124 GND
-- **SDA / SCL** → Notecard (internal to the Notecarrier; onboard pull-ups present)
-- **+VBAT** → 5V from HDR-15-5 (via Mojo on bench)
-- **Cellular u.FL** → pigtail to external SMA antenna outside the panel
-
-## 6. Notehub Setup
-
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) — it looks like `com.your-company.your-name:ev-charger-monitor`. Paste it into `firmware/ev_charger_session_monitor/ev_charger_session_monitor.ino` as the value of `PRODUCT_UID`, or pass it via a build flag (`-DPRODUCT_UID=\"com.your-company:your-project\"`).
-
-2. **Claim the Notecard.** Power the assembled unit. On first cellular connect the Notecard associates with your Notehub project automatically. The device will appear in **Devices** within a minute or two.
-
-3. **Create a Fleet per property.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration and routing. One fleet per property or tenant is the natural unit here — every charger at a given site shares the same line voltage, the same session-detection thresholds, and the same routing destinations. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can auto-assign new devices to the correct fleet at commissioning time; within a single Notehub project all devices share the same ProductUID, so fleet rules should discriminate on a device attribute that varies by site — for example, an installer-applied tag (e.g., `site:depot-north`), a serial/UID range reserved per property during provisioning, or location metadata populated during the first sync.
-
-4. **Set environment variables.** In Notehub, navigate to **Fleet → Environment** tab (or **Device → Environment** for a per-device override). Add or update any of the variables below; the device will fetch them on its next inbound sync — no reflash needed. All variables are optional; the firmware defaults shown below apply if you don't set them.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_sec` | `30` | Seconds between host wakes and Modbus meter polls. |
- | `report_interval_min` | `60` | Minutes between `charger_summary.qo` Notes. Changing this also re-applies `hub.set` so the Notecard's outbound cadence stays in sync. |
- | `session_threshold_w` | `500` | Active power (W) above which a charging session is considered open. Level 2 EVSE minimum pilot current (6 A × 208 V) is ~1250 W; 500 W sits safely below any real session while above charger standby draw. |
- | `session_end_count` | `3` | Consecutive below-threshold wakes before a session is closed. At the default 30-second interval, this is a 90-second grace period that absorbs brief power dips during EV charge-phase transitions. |
- | `voltage_present_v` | `85` | V_rms floor for mains-present classification. Readings below this threshold count against charger availability and start the `alert_offline_min` timer. 85 V is below the sag floor for both 120 V and 208/240 V feeds. |
- | `alert_offline_min` | `240` | Minutes without a mains-present reading before a `charger_alert.qo` Note fires. Set to `0` to disable offline alerting entirely. |
- | `modbus_slave_id` | `1` | Modbus slave address of the SDM120. The SDM120 factory default is 1; change if you have multiple meters on the same bus. |
- | `modbus_baud` | `9600` | Modbus RTU baud rate. Must match the SDM120's configured baud (factory default is 2400 on older units, 9600 on most current stock — check your meter's display or documentation). |
-
-5. **Configure routes.** Add at minimum one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `charger_session.qo` (real-time delivery to a fleet management system or dashboard webhook) and a second for `charger_summary.qo` (long-term analytics or historian). For sites where the mains-absent alert matters, add a third route for `charger_alert.qo` to an on-call or ticketing endpoint. The three Notefiles are separate specifically so each can be fanned out at a different urgency without filter logic in the route.
-
-### What you should see in Notehub
-
-Three event types matter:
-
-- **`_session.qo`** — automatic Notecard housekeeping on each cellular session. Confirms the radio is reaching Notehub.
-- **`charger_summary.qo`** — one per `report_interval_min` (default hourly). Sample body for a 60-minute window with one session:
- ```json
- {
- "sessions": 1,
- "total_kwh": 5.9,
- "avg_session_kwh": 5.9,
- "peak_w": 10320.0,
- "charging_min": 38,
- "idle_min": 22,
- "utilization_pct": 63.3,
- "available_min": 60,
- "availability_pct": 100.0,
- "sample_coverage_pct": 100.0
- }
- ```
- `total_kwh` is the SDM120's import-kWh register delta: the most recently valid closing reading minus the register value when the window opened, which is actual metered energy, not an estimate. If the meter poll fails on the exact wake when the summary fires, the most recent previously-valid reading is used as the closing value, so `total_kwh` remains accurate and the next window's kWh baseline is not corrupted. `charging_min` + `idle_min` always equals `total_min` by construction. `total_min` counts only wakes in which the meter returned valid data; wakes where all Modbus retries failed are excluded from the utilization accumulators so transient bus faults do not inflate idle time or distort `utilization_pct`. `utilization_pct` = `charging_min / total_min × 100`. `available_min` is the minutes during which V_rms was ≥ `voltage_present_v`; `availability_pct` = `available_min / elapsed_min × 100`, where `elapsed_min` is the full wall-clock window duration, including wakes where the meter was unreachable, which count as unavailable. `sample_coverage_pct` = `total_min / elapsed_min × 100` and signals the fraction of wall-clock window time that had valid meter readings; 100.0 means every poll succeeded, and a lower value flags Modbus or mains downtime during the window. In a fully-energized window where all polls succeeded and the charger was in use for 38 of 60 minutes: `available_min` = 60, `availability_pct` = 100.0, `sample_coverage_pct` = 100.0, and `utilization_pct` = 63.3.
-- **`charger_session.qo`** — one per completed charging session, emitted immediately (`sync:true`). Sample body:
- ```json
- {
- "session_kwh": 11.4,
- "duration_min": 63.5,
- "peak_w": 10320.0,
- "start_epoch": 1746283200,
- "timing_valid": true
- }
- ```
- `session_kwh` is the SDM120's import-kWh delta (close reading minus open reading): actual metered energy for this session. `peak_w` is the highest active-power reading seen during the session. Both fields are always valid regardless of `timing_valid`. `timing_valid` is `false` — and `duration_min` and `start_epoch` are emitted as `0` — when a session was opened before the Notecard completed its first cellular time sync. Suppress or specially flag session records with `timing_valid: false` in downstream analytics; `session_kwh` and `peak_w` can still be trusted for those records.
-- **`charger_alert.qo`** — emitted once (then suppressed until mains returns) when `alert_offline_min` minutes elapse without a mains-present V_rms reading. The timer reference is the last confirmed mains-present epoch, or (if mains has never been confirmed) the device's first valid epoch after commissioning, so the alert fires even for a circuit that has been dead since day one. Sample body:
- ```json
- {
- "alert": "mains_absent",
- "offline_min": 248
- }
- ```
-
-## 7. Firmware Design
-
-
-The firmware lives in three files inside `firmware/ev_charger_session_monitor/`:
-
-- [`ev_charger_session_monitor.ino`](firmware/ev_charger_session_monitor/ev_charger_session_monitor.ino) — `setup()`, `loop()`, and top-level app flow (cold-boot init, Modbus meter poll, sleep scheduling).
-- [`ev_charger_session_monitor_helpers.h`](firmware/ev_charger_session_monitor/ev_charger_session_monitor_helpers.h) — shared constants, the `State` struct, extern declarations, and function prototypes.
-- [`ev_charger_session_monitor_helpers.cpp`](firmware/ev_charger_session_monitor/ev_charger_session_monitor_helpers.cpp) — all helper implementations (Modbus polling, session state machine, Note emission, env-var handling, state persistence).
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Add the board manager URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**. Select **Blues Cygnet** as the board (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-- **`Blues Wireless Notecard`** library — [`note-arduino`](https://github.com/blues/note-arduino). Install via the Arduino Library Manager (`arduino-cli lib install "Blues Wireless Notecard"`) or search "Blues Wireless Notecard" in the IDE Library Manager. See the [note-arduino releases page](https://github.com/blues/note-arduino/releases) for any newer stable version.
-- **`ModbusMaster`** library — by Doc Walker, available in the Arduino Library Manager. Install with `arduino-cli lib install "ModbusMaster"` or search "ModbusMaster" in the IDE. Version ≥ 2.0.1. Provides the Modbus RTU master implementation used to communicate with the SDM120 over Serial1.
-
-**Flashing — Arduino IDE:** open `ev_charger_session_monitor.ino`, select the Cygnet board, hit **Upload**. The Notecarrier CX exposes the ST-Link interface on the USB cable — no external programmer needed.
-
-**Flashing — `arduino-cli`:** from the firmware directory,
-
-```bash
-# Typical modern stm32duino core
-cd firmware/ev_charger_session_monitor
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET ev_charger_session_monitor.ino
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* ev_charger_session_monitor.ino
-```
-
-If the upload fails with "Unknown board," first list available FQBN variants:
-
-```bash
-arduino-cli board listall | grep -i cygnet
-```
-
-and replace `STMicroelectronics:stm32:Blues:pnum=CYGNET` above with whatever `listall` reported. Replace `/dev/cu.usbmodem*` with the port the Notecarrier enumerates on your machine — typically `COMx` on Windows, `/dev/ttyACM*` on Linux, or `/dev/cu.usbmodem*` on macOS.
-
-Open the serial monitor at **115200 baud** after flashing. On cold boot you'll see `[app] cold boot — will configure Notecard`. Once the meter is connected, subsequent wakes print `[app] V=240.1 V P=0 W kWh=12.345` and then go quiet for 30 seconds as the host powers down. If the meter is absent, unpowered, or wired incorrectly you'll see `[app] WARN: Modbus voltage read error 0xE2` (response timeout) each wake. A `0x02` error indicates the slave responded but rejected the register address (typical of a slave-ID or register-map mismatch); other ModbusMaster error codes are documented in the library header.
-
-### 7.2 Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard configuration (`hub.set`, template registration, accelerometer disable) | `initNotecard`, `defineTemplates` |
-| RS-485 / Modbus initialization (every wake) | `initModbus` |
-| Time-filtered environment variable fetch | `fetchEnvOverrides` |
-| SDM120 Modbus RTU poll (V_rms, active power, import kWh) | `pollMeter` |
-| Epoch time from Notecard | `getEpoch` |
-| Session state machine (IDLE ↔ CHARGING) with availability tracking | `runSessionStateMachine` |
-| Per-session Note emission | `emitSessionNote` |
-| Hourly summary Note emission and window reset | `emitSummaryNote` |
-| Mains-absent alert emission | `emitOfflineAlert` |
-| State persistence and host sleep | `sleepHost` |
-
-### 7.3 Sensor reading strategy
-
-`pollMeter` issues three sequential Modbus RTU read transactions (Function Code 0x04 — input registers) to the SDM120, one for each required parameter: V_rms (register 0x0000), active power in Watts (register 0x000C), and cumulative import energy in kWh (register 0x0048). Each parameter is a big-endian IEEE-754 float stored across two consecutive 16-bit registers. A 20 ms pause between transactions is sufficient for the SDM120 to respond at 9600 baud (a full 9-byte response frame takes under 10 ms). If any read returns a non-zero Modbus error code, `pollMeter` retries the complete three-register sequence up to three times (50 ms inter-attempt pause). This absorbs the single-frame bus collisions and turnaround-timing faults that most commonly occur at power-on or after a brief conductor disturbance. If all three attempts fail (indicating the meter is unpowered, wiring is faulty, or the slave ID is misconfigured), `pollMeter` returns `valid = false`. When that happens, `runSessionStateMachine` suppresses all session-state transitions and all window accumulation (`charging_sec`, `idle_sec`) for that wake: a session in progress is neither extended nor closed (its `below_threshold_count` is not incremented), and no idle time is attributed to the failed sample. However, `window_elapsed_sec` still advances unconditionally, so the failed poll counts against `availability_pct` (as an unavailable interval) and reduces `sample_coverage_pct`. The mains-absent offline timer continues to advance independently (via `last_mains_epoch` in `setup()`), so sustained meter faults still trigger the `mains_absent` alert after `alert_offline_min` minutes.
-
-The SDM120 performs all V and I measurement and computation internally; the Cygnet reads the already-computed result registers. No ADC sampling, no voltage or power-factor assumptions, and no analog front-end components are required on the Notecarrier side.
-
-### 7.4 Event payload design
-
-`charger_summary.qo` uses a [template](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) that encodes each record as ~40 bytes on the wire (versus ~200 bytes as free-form JSON). At one record per hour per charger across a 50-unit fleet, this is meaningful over the lifetime of a 500 MB SIM. The template is registered once at cold boot and survives Notecard reboots.
-
-`charger_session.qo` and `charger_alert.qo` are intentionally left untemplated. Session Notes are infrequent (one per EV charge, typically 1–8 per day) and the alert body has a different shape from the session body, so keeping them untemplated avoids the schema-conflict issue that arises from mixing two different body shapes under a single template.
-
-Template definition snippet:
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", FILE_SUMMARY);
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "sessions", 12); // 2-byte signed int
-JAddNumberToObject(body, "total_kwh", 14.1); // 4-byte IEEE-754 float — metered
-JAddNumberToObject(body, "avg_session_kwh", 14.1);
-JAddNumberToObject(body, "peak_w", 14.1); // measured peak active power
-JAddNumberToObject(body, "charging_min", 12);
-JAddNumberToObject(body, "idle_min", 12);
-JAddNumberToObject(body, "utilization_pct", 14.1);
-JAddNumberToObject(body, "available_min", 12); // minutes with mains present
-JAddNumberToObject(body, "availability_pct", 14.1); // available_min / elapsed_min × 100
-JAddNumberToObject(body, "sample_coverage_pct", 14.1); // total_min / elapsed_min × 100
-notecard.sendRequest(req);
-```
-
-**Template notation explained:** `12` means a 2-byte signed integer (range -32,768 to 32,767); `14.1` means a 4-byte IEEE-754 float with 1 decimal place in transmission. The template compresses each full summary record from roughly 200 bytes of free-form JSON to about 40 bytes on the wire, meaningful over a 500 MB SIM lifetime at one record per hour per device.
-
-### 7.5 Low-power strategy
-
-The device is grid-tied, so absolute energy budget is not critical, but a well-disciplined sleep pattern still matters: it keeps the enclosure cooler, reduces component stress, and creates a firmware pattern that can be ported directly to a battery- or solar-backed variant without a rewrite. After each 30-second wake, the host serializes its `State` struct into Notecard flash and issues `NotePayloadSaveAndSleep`, which triggers `card.attn` to cut host power for the next interval. The Notecard sits in its 8–18 µA idle between cellular sessions. The Modbus poll adds roughly 100–200 ms to each wake (three transactions at 9600 baud with 20 ms pauses) but the host MCU is still only active for about one to two seconds every 30 seconds, and the radio runs roughly once an hour plus any session-end sync events.
-
-`hub.set` is configured for `periodic` mode with `outbound` matching `report_interval_min` (default 60 min) and `inbound` at 120 min. Session end Notes set `sync:true`, which causes the Notecard to open a cellular session immediately rather than waiting for the outbound timer. With the default `session_end_count=3` and a 30-second sample interval, the state machine requires 3 consecutive below-threshold wakes (a 90-second grace period) before formally closing the session and queuing the Note. Cellular sync then adds roughly 15–60 seconds. End-to-end, operators should expect a completed session to appear in Notehub **roughly 2–3 minutes after the car unplugs** with default settings. Reducing `session_end_count` (e.g., to `1`) shortens the grace period at the cost of increased false-close risk during EV charge-phase transitions.
-
-### 7.6 Retry and error handling
-
-- `pollMeter` retries the complete three-register Modbus sequence up to three times per wake before declaring a poll failure. When all retries fail, `runSessionStateMachine` suppresses all session-state transitions and all window accumulation (`charging_sec`, `idle_sec`) for that wake — a session already in progress is held open (its `below_threshold_count` is not incremented), and no idle time is attributed to the failed sample. `window_elapsed_sec` still advances (as it does every wake), so the failed poll counts against `availability_pct` in the denominator and also reduces `sample_coverage_pct`. The mains-absent offline timer continues to advance regardless, so a sustained meter fault still triggers the `mains_absent` alert after `alert_offline_min` minutes.
-- The first `sendRequestWithRetry(req, 5)` in `initNotecard` papers over the cold-boot I²C race documented in the note-arduino library.
-- `fetchEnvOverrides` passes the stored `env_last_modified` timestamp to `env.get`'s `time` filter. If nothing has changed since the last fetch, the Notecard returns an empty response body, and the function returns immediately — no parsing, no config updates, minimal I²C overhead.
-- `getEpoch` returns 0 if the Notecard hasn't yet synced and obtained time. The summary Note is guarded by `now > 0` and `window_start_epoch > 0`, so it does not fire before a cellular session establishes. The mains-absent alert is guarded by `offline_ref > 0`, where `offline_ref` is the last mains-present epoch, or `window_start_epoch` when mains has never been confirmed — so the alert is suppressed until the device has a valid clock and the configured absence duration has genuinely elapsed.
-- `emitSessionNote` sets `timing_valid = false` when the `start_epoch` parameter is 0 — which happens when a session was opened before the Notecard completed its first cellular time sync. In that case `duration_min` and `start_epoch` would be wildly incorrect, so both are emitted as 0 and `timing_valid: false` is included in the Note body to signal suppression to downstream consumers. `session_kwh` and `peak_w` are unaffected and always emitted normally.
-- Env var changes to `report_interval_min` re-apply `hub.set` via `applyHubCadence()`. The `hub_cadence_dirty` flag is set before the attempt and cleared only on confirmed success, so a single transient I²C failure cannot leave the Notecard permanently on the old outbound cadence; `setup()` retries on every subsequent wake until the cadence is confirmed updated — mirroring the `notecard_configured` retry pattern.
-- When `emitSessionNote()` fails at session end, the completed payload is stored in a dedicated pending-note struct (`pending_session_*`) and `session_active` is cleared immediately. The pending note is retried at the top of `runSessionStateMachine()` on each subsequent wake, but the function always falls through to normal state-machine processing regardless of whether the retry succeeded — so a back-to-back charging session during a transient Notecard fault is detected and opened correctly rather than being silently blocked by a retry loop. **Retry-path window attribution:** if the hourly summary fires and resets the window accumulators between the session-end failure and a subsequent successful retry, the session's count and completed-session kWh are attributed to the newer window rather than the window in which the session actually ended. This edge case is documented further in §10.
-- `alert_offline_min` accepts `0` (set the env var to `0` to disable mains-absent alerting), or any positive integer to adjust the threshold. The firmware checks for key presence before applying the value, so an absent env var leaves the current setting unchanged.
-
-### 7.7 Key code snippet: session-end Note with immediate sync
-
-When the session state machine closes a session, it computes `session_kwh` from the meter's import-kWh register delta, stores the payload in a pending-note struct (`pending_session_*`), and clears `session_active` immediately before calling `emitSessionNote`. All payload values are passed as explicit parameters so the function works identically at first-close time and during pending-note retries — after session state has been cleared and possibly overwritten by a new session. A `timing_valid` guard protects against sessions opened before the Notecard had a valid epoch: if `start_epoch` is 0, `duration_min` and `start_epoch` would be wildly incorrect, so they are emitted as 0 and `timing_valid` is set to `false`. `session_kwh` and `peak_w` are always valid regardless.
-
-```cpp
-bool emitSessionNote(float kwh, float peak_w,
- uint32_t start_epoch, uint32_t end_epoch) {
- bool timing_valid = (start_epoch > 0 && end_epoch >= start_epoch);
- uint32_t dur_sec = timing_valid ? (end_epoch - start_epoch) : 0;
-
- J *req = notecard.newRequest("note.add");
- JAddStringToObject(req, "file", FILE_SESSION);
- JAddBoolToObject(req, "sync", true);
- J *body = JAddObjectToObject(req, "body");
- JAddNumberToObject(body, "session_kwh", kwh); // metered import-kWh delta
- JAddNumberToObject(body, "duration_min", (float)dur_sec / 60.0f);
- JAddNumberToObject(body, "peak_w", peak_w); // measured peak active power
- JAddNumberToObject(body, "start_epoch", timing_valid ? (double)start_epoch : 0.0);
- JAddBoolToObject(body, "timing_valid", timing_valid);
- // ...
-}
-```
-
-### 7.8 Key code snippet: state persistence across sleep
-
-`NotePayloadSaveAndSleep` serializes the entire `State` struct into Notecard flash, then issues the `card.attn` sleep request. On the next wake, the host enters `setup()` from cold and `NotePayloadRetrieveAfterSleep` rehydrates it, so the session start kWh baseline, peak power, and hourly window state survive the host being fully powered down.
-
-```cpp
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, state.sample_interval_sec, NULL);
-```
-
-### 7.9 Key code snippet: Modbus meter poll
-
-`pollMeter` reads three SDM120 input-register pairs in sequence. Each pair is a big-endian IEEE-754 float; `regsToFloat` reassembles the two 16-bit halves via a union.
-
-```cpp
-// Read active power (0x000C, 2 registers → float32)
-uint8_t rc = node.readInputRegisters(SDM_REG_POWER, 2);
-if (rc != ModbusMaster::ku8MBSuccess) {
- Serial.print("[app] WARN: Modbus power read error 0x");
- Serial.println(rc, HEX);
- return false;
-}
-float pw = regsToFloat(node.getResponseBuffer(0), node.getResponseBuffer(1));
-out->power_w = (pw > 0.0f) ? pw : 0.0f; // clamp export to zero
-
-// Helper: reassemble two 16-bit Modbus registers into IEEE-754 float
-static float regsToFloat(uint16_t hi, uint16_t lo) {
- union { float f; uint32_t u; } v;
- v.u = ((uint32_t)hi << 16) | (uint32_t)lo;
- return v.f;
-}
-```
-
-## 8. Data Flow
-
-
-
-
-**Collected.** Every `sample_interval_sec` (default 30 s): V_rms, active power (W), and cumulative import kWh from the SDM120 via three Modbus RTU reads. The SDM120 performs all measurement and computation internally; the firmware reads the already-computed result registers.
-
-**Session tracking.** The state machine opens a session when `active_power_w ≥ session_threshold_w` and closes it after `session_end_count` consecutive below-threshold wakes. Session energy is computed at close time from the meter's cumulative import-kWh register:
-
-```
-session_kwh = import_kwh_at_close − import_kwh_at_open
-```
-
-This is actual metered energy — no voltage or power-factor approximation. Window total kWh is computed the same way at summary time:
-
-```
-window_total_kwh = import_kwh_now − import_kwh_at_window_open
-```
-
-**Availability tracking.** Each wake, `window_elapsed_sec` grows by `sample_interval_sec` unconditionally (wall-clock time). Additionally, if `meter.valid` and `V_rms ≥ voltage_present_v`, `window_available_sec` also grows by `sample_interval_sec`. When the meter is unreachable (meter unpowered, wiring fault, or all Modbus retries failed), `meter.valid` is false: `window_elapsed_sec` still grows but `window_available_sec` does not, so those wakes count as unavailable in `availability_pct` = `available_min / elapsed_min × 100`. `sample_coverage_pct` = `total_min / elapsed_min × 100` provides a data-quality signal — 100.0 means every poll in the window succeeded; a lower value quantifies how much of the window lacked valid meter readings.
-
-**Transmitted.**
-- `charger_session.qo` — one Note per completed session, emitted immediately (`sync:true`), carrying metered session kWh (import-kWh delta), duration in minutes, peak active power (`peak_w`), UTC epoch of session start, and a `timing_valid` flag. `session_kwh` and `peak_w` are always valid. When `timing_valid` is `false` (session opened before the first time sync), `duration_min` and `start_epoch` are emitted as 0 and must be treated as invalid by downstream consumers. This is the record that answers "how much energy did Charger 3 deliver on Tuesday at 8 a.m.?"
-- `charger_summary.qo` — one templated Note per `report_interval_min` (default hourly), queued to the Notecard and transmitted on the outbound sync. Contains session count, metered total kWh (import-kWh register delta using the last valid closing reading), per-session average kWh, peak active power (`peak_w`), active minutes (`charging_min`), inactive minutes (`idle_min`), utilization percentage, and availability fields: `available_min` (minutes with V_rms ≥ `voltage_present_v`), `availability_pct` = `available_min / elapsed_min × 100` (wall-clock denominator so invalid-meter wakes count as unavailable), and `sample_coverage_pct` = `total_min / elapsed_min × 100` (data-quality signal). `charging_min` counts minutes with active power above the session threshold plus any grace-period wakes; `idle_min` is the remainder; both use only valid-poll wakes as the denominator so `utilization_pct` is unaffected by Modbus faults. `available_min` reflects the minutes where the charger circuit was confirmed energized. This is the record that answers both "do we need more chargers?" and "was this charger actually available to be used?"
-- `charger_alert.qo` — emitted once if `alert_offline_min` elapses without a mains-present V_rms reading, then suppressed until mains returns. This is a definitive signal: the SDM120 requires mains power to respond on Modbus, so consecutive failed reads at `9600` baud combined with a below-threshold V_rms reliably indicate a tripped breaker, power loss, or sustained meter fault rather than a healthy charger sitting idle.
-
-**Routed.** Notehub fans the three Notefiles out separately: `charger_session.qo` to whatever real-time channel the operator uses (fleet dashboard, CMMS work order, alerting service), `charger_summary.qo` to a long-term analytics store for trend analysis ("was March utilization higher than January?"), and `charger_alert.qo` to an on-call or ticketing endpoint.
-
-**Trigger logic summary:**
-- Session opens: `active_power_w ≥ session_threshold_w` for one sample
-- Session closes: `session_end_count` consecutive below-threshold samples
-- Hourly summary: wall-clock window of `report_interval_min` elapsed
-- Mains-absent alert: `alert_offline_min` > 0 and `alert_offline_min` minutes elapsed since the last confirmed mains-present reading (or device commissioning if mains has never been confirmed); fires once; resets when mains returns; disabled when `alert_offline_min = 0`
-
-## 9. Validation and Testing
-
-
-**Expected steady-state cadence.** In normal use, a charger with two or three sessions per day generates: one `charger_summary.qo` per hour (24/day), one `charger_session.qo` per session (2–8/day), and zero `charger_alert.qo` events. In a healthy window, `sample_coverage_pct` should be 100.0 (every Modbus poll succeeded). If the summary Note shows `utilization_pct` consistently above 80%, the charger is effectively saturated; if `availability_pct` is below 95%, the charger circuit has been losing power frequently and maintenance is warranted.
-
-**Bench simulation.** Before installing in a live panel, test session detection on the bench by connecting the SDM120 to a live AC circuit (observe all electrical safety precautions) with a resistive load. A 500 W or larger resistive load will exceed the default `session_threshold_w = 500` W. With the SDM120 powered and the RS-485 connection wired correctly, you should see the serial monitor printing `[app] V=XXX.X V P=XXXX W kWh=X.XXX` every 30 seconds. Plug the load in; watch the monitor print `[app] session STARTED`. Leave it running for a few minutes, then unplug. Count three wakes printing power near 0 W — the 90-second grace period (`session_end_count = 3`, 30 seconds each). The `charger_session.qo` Note queues with `sync:true` once the session formally closes; allow roughly 2–3 minutes from unplugging for the Note to appear in Notehub (90 seconds grace period + 15–60 seconds cellular sync).
-
-If you do not have a live SDM120 available for initial bring-up, you can verify the Modbus stack by connecting the RS-485 bus to a Modbus RTU simulator (e.g., a PC running Modbus Poll or a second Arduino running a Modbus slave sketch). The firmware will print `[app] WARN: meter poll failed this wake` for each failed read but will still run the Notecard initialization, env-var fetch, and sleep cycle correctly.
-
-**Using Mojo to validate power behavior.** Insert the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the HDR-15-5 5V output and the Notecarrier CX `+VBAT` pad for bench bring-up. Published Notecard current figures from the [low-power firmware design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/):
-
-| Phase | Expected current |
-|---|---|
-| Notecard idle (host cut by ATTN, radio off) | 8–18 µA @ 5V |
-| Host active (Modbus poll + I²C, ~1–2 seconds per wake) | measure with Mojo. See Note below |
-| Notecard cellular session (LTE Cat-1 bis) | ~200–400 mA average, ≤2A peak bursts |
-
-The host MCU active-phase current depends on clock configuration and peripheral state; use the Mojo trace to measure it rather than relying on a prior estimate. A healthy Mojo trace looks like this: a near-zero baseline punctuated by brief 1–2-second elevated blips at each 30-second wake, with one longer 15–30-second burst at ~200–400 mA every hour (the outbound sync) plus additional shorter bursts at session ends. Total daily energy varies with session count, session frequency, and cellular signal quality; characterize it on the bench with a known load pattern and use that measured figure as your deployment baseline.
-
-Two failure patterns are immediately visible on the Mojo:
-- **Host never sleeping:** continuous elevated baseline instead of 30-second gaps. Almost always means `NotePayloadSaveAndSleep` returned early (check that the ATTN pin is connected on the Notecarrier CX and that the Notecard's I²C is responding).
-- **Radio never going idle:** long continuous transmit blocks rather than clean bursts. Indicates the Notecard is not entering `periodic` mode idle — verify the `hub.set` call succeeded at cold boot by watching serial output.
-
-Mojo is bench-validation tooling — deployed units running from a panel supply don't need it.
-
-## 10. Limitations and Next Steps
-
-
-A meter-based monitor and an OCPP-integrated monitor answer different questions, and this reference design deliberately answers the simpler one: how is the circuit being used, regardless of charger brand or network. The trade-offs that come with that choice (heuristic session boundaries, no per-driver records, single-phase coverage) are listed below, alongside the natural production extensions.
-
-### Design Boundaries
-
-Each boundary below is a place where the meter-based approach trades authoritative session detail for brand-agnostic simplicity, and where a production deployment may want to add a meter, a phase, or an OCPP back-end.
-
-**Energy-meter integration is SDM120 Modbus RTU, not OCPP.** This project implements a hardware energy-meter integration on the charger's AC feed. It does not implement OCPP or direct charger-network integration and therefore does not provide per-driver, per-RFID, or charger-network session records. Session boundaries are power-threshold heuristics (`active_power_w ≥ session_threshold_w`), not authoritative OCPP `StartTransaction`/`StopTransaction` events. Sites that additionally require per-driver records or OCPP-correlated data should implement an OCPP back-end integration alongside this meter-based monitor. See the production next steps at the end of this section.
-
-**SDM120 ampacity and neutral requirement.** The SDM120-Modbus direct-connect variant is rated 45 A continuous. It covers 32 A EVSE (40 A breaker) and 40 A EVSE (50 A breaker). For 48 A or 60 A EVSE circuits, substitute the **SDM120CT** variant (uses an external split-core CT; rated for any primary current, CT-dependent) or the **SDM230-Modbus** (100 A direct-connect). Additionally, the SDM120 requires a neutral connection for its internal voltage measurement. In a subpanel where only two line conductors are available (e.g., a 240 V line-to-line feed with no neutral), the SDM120 cannot measure voltage and will not operate correctly; use a single-phase meter that accepts a two-wire (L-L) supply voltage input instead, or run a neutral conductor from the main panel.
-
-**Availability is derived from V_rms, not OCPP status.** `available_min` and `availability_pct` are derived from the SDM120's V_rms register. When V_rms ≥ `voltage_present_v`, the charger circuit is classified as energized. Wakes where the meter poll fails entirely (because mains is absent, wiring is faulted, or all Modbus retries fail) also count against availability: they contribute to the wall-clock `elapsed_min` denominator but not to `available_min`, so `availability_pct` correctly falls below 100 % whenever the circuit was offline for any reason. `sample_coverage_pct` distinguishes sustained meter faults from genuine mains absence: if `availability_pct` is low and `sample_coverage_pct` is also low, the charger was likely offline; if `availability_pct` is low but `sample_coverage_pct` is near 100, the V_rms readings were valid and indicate a prolonged low-voltage or brownout condition. This design does **not** detect an EVSE that is powered but internally faulted (e.g., a GFI trip within the EVSE, a J1772 pilot fault, or an EVSE error state); those faults leave the circuit energized and appear as idle time (`charging_min = 0, available_min = elapsed_min`). Sites requiring per-charger EVSE fault detection should supplement this meter monitor with OCPP status-notification integration. Within those limits, `availability_pct` provides a useful signal: sustained `availability_pct < 100` in a summary window confirms a real mains interruption during that period, and `availability_pct = 100` with `utilization_pct = 0` indicates the charger was powered but unused.
-
-**Metered accuracy is Class 1, not revenue-grade.** The SDM120 is IEC 62053-21 Class 1, meaning ±1% accuracy under reference conditions. This is sufficient for utilization monitoring and tenant cost allocation, but falls short of revenue-grade Class 0.5 or 0.2 instruments required for utility billing. For billing applications, substitute a revenue-grade instrument.
-
-**Monitoring is single-phase only.** This design monitors one single-phase EVSE circuit. Three-phase 208 V or 480 V commercial charging installations (typically DCFC, DC fast chargers) require a three-phase energy meter and are not covered by this reference design.
-
-**Session detection is power-threshold only.** The firmware does not distinguish between "EV actively charging at full rate," "EV plugged in but in top-off mode at reduced power," and "EV plugged in but the BMS has paused charging." All three produce above-threshold active power and appear as a single continuous session. An OCPP-integrated data source would provide the authoritative session state.
-
-**Sessions spanning summary windows, retry-path attribution, and single-slot data loss.** Window `total_kwh` uses the meter-register delta and captures all energy in the window period regardless of session boundaries. However, `window_sessions` counts only sessions that *close* in the window — a session still in progress when the summary fires will be counted in the window where it ends. On the retry path: if `emitSessionNote()` fails at session close and the pending retry only succeeds after the summary window has already reset, `window_sessions` and `window_completed_session_kwh` are attributed to the newer window. **Single-slot data loss:** the pending-session store holds exactly one completed-session payload. If a second session closes while the first Note has not yet been successfully queued (Notecard fault lasting longer than one complete charging session), the older session record is **overwritten and permanently lost**. A `[app] WARN: overwriting unconfirmed pending session Note` line is printed to serial. Production deployments requiring guaranteed per-session delivery should replace the single-slot pending store with a small ring buffer. See the code comment in `ev_charger_session_monitor_helpers.h`.
-
-**No Notecard outboard firmware update wired.** The firmware does not configure [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) for over-the-air host firmware updates. Firmware updates require a physical USB connection.
-
-### Production Next Steps
-
-Once the basic meter monitor is running, these extensions move it toward billing-grade accuracy and multi-port, OCPP-correlated coverage, roughly from the most immediately useful to the most integration-dependent.
-
-**Upgrade to a revenue-grade meter.** Swap in a Class 0.5 or 0.2 DIN-rail energy meter (e.g., EASTRON SDM72D or equivalent) for utility-billing accuracy.
-
-**Add a second meter for two-port chargers.** For a two-port charger (two EVSE circuits in the same panel), add a second SDM120 on a shared RS-485 bus using a different Modbus slave ID; extend the firmware to poll both meters and emit per-port session and summary Notes.
-
-**Implement OCPP session correlation.** Integrate with the charger's OCPP back-end via its WebSocket/HTTP interface or a network proxy to capture `StartTransaction` / `StopTransaction` events, then cross-reference those records with energy-meter session data to get per-driver, per-RFID, per-session energy records.
-
-**Wire Notecard Outboard DFU.** Connect it to the Cygnet's boot/reset pins for over-the-air host firmware updates across the deployed fleet.
-
-**Replace the single-slot pending store with a ring buffer.** A small ring buffer (4–8 slots) in the `State` struct guarantees per-session delivery even during extended Notecard faults. See the extension Notes in `ev_charger_session_monitor_helpers.h`.
-
-## 11. Summary
-
-The fleet manager who used to wonder whether Bay 3 was actually being used now has the answer in Notehub: per-session metered kWh, peak demand, session duration, port utilization, and charger availability, hourly. The SDM120 measures real energy delivery and mains voltage; the Notecard dials home over LTE Cat-1 bis with no IT involvement, no AP to pair to, and no per-site network setup. Session records and availability summaries start landing within minutes of commissioning. The signal that emerges answers two questions at the same time (*how are the chargers being used* and *how reliably are they available*) and the answers cleanly separate the problems they imply. A site running at 85% utilization with 100% availability needs a scheduling policy, not more hardware. A site at 72% availability needs a maintenance visit. A site where two chargers are maxed out while six others sit idle has a fleet routing problem. None of those signals was visible before, and the same SKU works on any Level 2 EVSE ever installed.
+A cellular retrofit for Level 2 EV chargers that reports per-session metered energy, hourly utilization, and mains-offline alerts without modifying charger hardware or enrolling in OCPP.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/ev-charger-session-utilization-monitor/)**
diff --git a/80-ev-charger-session-utilization-monitor/banner.png b/80-ev-charger-session-utilization-monitor/banner.png
deleted file mode 100644
index 54853a64..00000000
Binary files a/80-ev-charger-session-utilization-monitor/banner.png and /dev/null differ
diff --git a/80-ev-charger-session-utilization-monitor/diagrams/01-system-architecture.svg b/80-ev-charger-session-utilization-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index d8bc5b16..00000000
--- a/80-ev-charger-session-utilization-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/80-ev-charger-session-utilization-monitor/diagrams/02-wiring-assembly.svg b/80-ev-charger-session-utilization-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 883e6083..00000000
--- a/80-ev-charger-session-utilization-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,73 +0,0 @@
-
diff --git a/80-ev-charger-session-utilization-monitor/diagrams/03-data-flow.svg b/80-ev-charger-session-utilization-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index 0cbefed5..00000000
--- a/80-ev-charger-session-utilization-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
diff --git a/81-commercial-tenant-sub-metering-bridge/README.md b/81-commercial-tenant-sub-metering-bridge/README.md
index 27590cfa..9b3d74f3 100644
--- a/81-commercial-tenant-sub-metering-bridge/README.md
+++ b/81-commercial-tenant-sub-metering-bridge/README.md
@@ -1,608 +1,5 @@
-# Commercial Tenant Energy Monitoring Bridge
+# Commercial Tenant Sub-Metering Bridge
-
+A cellular retrofit for multi-tenant commercial buildings that gives landlords per-tenant interval energy usage and 15-minute demand readings each hour, on a data channel tenants cannot access.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Commercial%20Tenant%20Energy%20Monitoring%20Bridge).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/tenant_sub_meter/](firmware/tenant_sub_meter/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a cellular [energy monitoring](https://blues.com/solutions-energy-monitoring/) retrofit for multi-tenant commercial buildings. Four Rogowski coil sensors with per-channel active integrator circuits on a panel, a Blues Notecard Cell+WiFi, and a Notecarrier CX with its onboard Cygnet host, and a landlord gains per-tenant estimated interval energy (Wh), 15-minute blocked-average demand (W), and a per-channel fault bitmask every hour, on a data channel the tenants can neither see nor interfere with, without running a single network cable through the building.
-
-## 1. Project Overview
-
-**The problem.** A landlord of a multi-tenant commercial building (a flex-industrial park, a shared professional building, an office strip) wants to allocate electricity costs to each tenant based on their actual consumption rather than splitting the utility bill by square footage or some other proxy. The fair approach is sub-metering: install a watt-hour meter on each tenant's branch circuit and read it monthly.
-
-The problem is the cost and the complexity. Commercial sub-meters require a wired or networked path to get their data off the panel and into the billing system. An older building with four or eight tenants has no data infrastructure of any kind in the electrical room, and retrofitting it means negotiating with tenants, coordinating with the utility, and running cable through occupied ceilings. Most landlords give up and keep allocating by square footage.
-
-This project takes a different path. Instead of installing meters with network cards, it clips Rogowski coil sensors onto each tenant's existing feed conductors (no wire cuts, no conduit, no utility coordination) and uses a Notecard to move the data to the cloud. A voltage transducer provides a coherent voltage reference so the device computes active power (W) and estimated interval energy (Wh) from sequential interleaved V×I measurements, rather than an estimated figure derived from an assumed voltage and fixed power factor. The system installs in an afternoon and costs a fraction of a traditional per-tenant metered retrofit.
-
-**What this project delivers, and what it does not.** This design provides estimated tenant load allocation data useful for proportional charge-back, load profiling, and identifying unusually high-consumption tenants. It does **not** replace a utility-certified sub-meter. Readings are derived from periodic active-power snapshots (one ~200 milliseconds V×I mean every five minutes) rather than continuous integration, and the sequential (non-simultaneous) V/I sampling introduces a small systematic error at low power factors. For internal bill-back purposes the approach is accurate enough to support proportional allocation between tenants. For billing applications requiring certified metering accuracy, for example, sub-metering subject to utility-tariff accuracy regulations, a dedicated simultaneous-sampling energy-metering IC or certified sub-meter interface should replace this approach.
-
-**Why Notecard, and why cellular specifically.** The "why cellular" argument in most IoT applications is about location: the asset is remote, or on a rooftop, or in a place where running Ethernet is impractical. This application is different. The panel room is usually indoors, and there is WiFi somewhere nearby. The problem is *whose* WiFi it is.
-
-
-
-In a multi-tenant building, the WiFi access points are almost always under tenant control. A retail tenant pays for their own ISP and runs their own router; a professional office tenant does the same. The building owner typically has no WiFi at all, or if they do, tenants have access to it. Routing billing telemetry through any network the tenants can reach is a political and legal problem: a savvy tenant can see what's being transmitted, disrupt the connection, or argue that the data was tampered with. The landlord needs a data channel that is:
-
-1. 100% landlord-controlled: the tenant cannot touch it
-2. Invisible to tenants: the tenant cannot see or intercept it
-3. Operationally simple: no IT setup, no passwords, no tenant cooperation
-
-A cellular Notecard meets all three criteria. It's a SIM-bearing module that calls home to the [Blues Notehub](https://blues.com/notehub/) cloud service over the cellular network, a connection the tenant has no access to and no visibility into, exactly the same way the building's alarm system uses its own GSM dialout. There is no form to fill out, no AP to pair to, and no IT ticket to raise with any tenant. The data channel is as landlord-owned as the panel itself.
-
-This is not a niche edge case. Billing disputes are among the most contentious issues in commercial tenancy, and the architecture matters: a meter whose data travels over the tenant's network is a meter whose readings a clever tenant can plausibly dispute. A cellular Notecard is the only architecture that eliminates that dispute by design.
-
-**Deployment scenario.** A small, weatherproof or panel-rated enclosure mounted inside or alongside the building's electrical panel. Four Rogowski coil sensors — one per tenant feed — with the coil lead running to the enclosure via a short cable gland. A voltage transducer taps the same panel bus for a building voltage reference. Line power sourced from a spare breaker on the same panel. No rented rack space, no network closet, no IT involvement from anyone.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** All four channels share one Cygnet STM32L433 host on the Notecarrier CX, and that host wakes on a five-minute cadence (default) to do the same thing every cycle: read each active tenant channel by interleaving ADC samples between the voltage transducer output and the Rogowski integrator output, compute active power (W) via a V×I cross-product, and accumulate estimated interval energy (Wh), 15-minute blocked-average demand (W), and a per-channel hardware-fault bitmask into a RAM struct that's persisted to Notecard flash across sleep. Once per hour the host emits a summary Note carrying the accumulated energy, peak demand, and fault flags. Configuration lives in [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) distributed by Notehub (sensor sensitivity, voltage scaling, all of it) so the landlord can adjust calibration in the field without a reflash.
-
-**Notecard responsibilities.** The Notecard is the cellular muscle and the sleep gate. It queues [Notes](https://dev.blues.io/api-reference/glossary/#note) locally, wakes the cellular radio on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 min), and flushes the entire queue to Notehub in a single cellular session. Environment-variable changes pushed from Notehub come back on its `inbound` cadence (default 120 min), and [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) sleep mode cuts host power between samples so the Cygnet draws essentially zero current from the supply between wakeups.
-
-**Notehub responsibilities.** Once Notes arrive at [Notehub](https://notehub.io) over the Notecard's embedded global SIM, they're stored durably and routed downstream. Notehub is also the **sole monthly aggregation path**: a billing or property-management platform sums each tenant's `t*_wh` values across all hourly `meter_summary.qo` events for a device over any billing period using the [Notehub Event Query API](https://dev.blues.io/api-reference/notehub-api/). The device never emits a month-end Note and never tries to compute a billing total; the hourly event stream in Notehub is the complete, authoritative record.
-
-[Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices per property so a property manager with multiple buildings can push sensor calibration settings per-building without touching individual devices. A Notehub HTTP or MQTT route can forward each `meter_summary.qo` Note directly to the property-management or billing platform as it arrives.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-Four independent sub-meter channels, each with isolated analog inputs and synchronized 100 Hz ADC sampling, feed a Cygnet STM32L4 running accumulation windows across configurable billing periods. Per-tenant energy (Wh), peak demand (W), and hardware-fault status flow upward via Notecard on a configurable outbound cadence. On-device queuing, sleep gating, and fault-aware transmission tolerate carrier dropouts and cloud-side downtime without data loss. No external flash or EEPROM required; state persistence uses the Notecard's on-device storage.
-
-Here is a sample Note this device emits:
-```json
-{
- "t1_wh": 482.3,
- "t1_demand_w": 3120.0,
- "t2_wh": 214.1,
- "t2_demand_w": 1480.5,
- "t3_wh": 673.8,
- "t3_demand_w": 4220.0,
- "t4_wh": 149.2,
- "t4_demand_w": 980.0,
- "fault_mask": 0
-}
-```
-
-
-## 4. Hardware Requirements
-
-### Prototype / Bench Evaluation BOM
-
-Use this BOM for firmware bring-up, calibration, and evaluation. The ZMPT101B module is not agency-listed for permanent installation inside a commercial panel enclosure; replace it with an agency-listed isolated voltage transducer before deploying to a live panel — see [§11 Limitations and Next Steps](#11-limitations-and-next-steps) for the full production voltage-sensing path, including hardware requirements and firmware adaptation Notes.
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with onboard Cygnet STM32L433 host; six analog inputs accommodate all four current channels plus the voltage channel with one spare. No separate MCU needed. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular is required — tenant WiFi and building WiFi are off-limits for billing telemetry. WiFi is present on the MBGLW SKU but intentionally unused in this design. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(commissioning and bench validation only — not deployed to the field)* | 1 | Coulomb counter for validating the sleep/wake current profile during bring-up. See [§10](#10-validation-and-testing). |
-| [Magnelab RCT-1800-000](https://magnelab.com/product/flexible-ac-rogowski-coil-rct-1800-000/) flexible Rogowski coil ([spec sheet](https://magnelab.com/wp-content/uploads/2015/07/RopeCT-Series-AC-Current-Sensor-Rogoskies-Coil-Spec-Sheet.pdf)) — or equivalent bare-output flexible Rogowski coil rated for the installed conductor capacity | 4 | Non-invasive clip-on coil; no conductor cut or conduit penetration required. The RCT-1800-000 handles a wide range of conductor diameters; its flexible loop closes with a snap-together coupler. Its dI/dt output requires the per-channel active integrator described in [§5](#5-wiring-and-assembly). One coil per tenant. Any Rogowski coil producing a proportional dI/dt output can be used — calibrate `rogowski_amps_per_volt` to match the installed coil's rated output sensitivity. |
-| [Microchip MCP6004](https://www.microchip.com/en-us/product/MCP6004) ([datasheet](https://ww1.microchip.com/downloads/en/DeviceDoc/MCP6001-1R-1U-2-4-1-MHz-Low-Power-Op-Amp-DS20001733L.pdf)) quad op-amp, single-supply 2.5 V–5.5 V — use **MCP6004-I/P** (DIP-14) for breadboard/perfboard builds or **MCP6004-I/SL** (SOIC-14) for PCB layouts | 1 | Drives all four active Miller integrators from a single package. Single-supply operation from the 3.3 V Cygnet rail; rail-to-rail input/output; GBW 1 MHz is well above the 60 Hz integrator corner frequency. |
-| 10 kΩ resistor 1% (R_in, integrator input) | 4 | One per integrator channel; sets the integrator time constant with C_f. |
-| 1 MΩ resistor 1% (R_f, integrator DC-stabilization) | 4 | One per integrator channel; limits DC gain of the Miller integrator to prevent output rail saturation from op-amp offset drift. |
-| 10 nF film or C0G capacitor (C_f, integrator feedback) | 4 | One per integrator channel; paired with R_in and R_f to form the frequency response. |
-| 100 kΩ resistor 1% (bias half-rail divider) | 10 | Two per input channel (four current + one voltage = five channels); form the half-rail divider that centers each AC signal at 1.65 V for the single-supply ADC. |
-| 10 µF electrolytic capacitor | 5 | One per channel. On current-sensor channels (A0–A3): shunts AC noise from the op-amp IN+ bias node to GND. On the voltage channel (A4): wired in series between ZMPT101B VOUT and ADC A4 to AC-couple the module output onto the 1.65 V bias point (electrolytic polarity: + toward VOUT). |
-| ZMPT101B voltage sensor module — **VCC/GND/VOUT pinout, 5 V supply** (**bench/prototype only — not agency-listed for mains use in a commercial panel enclosure**; Zhongmet Technology ZMPT101B transformer IC on a PCB with onboard burden resistor and trim pot; see the [ZMPT101B IC datasheet](https://www.lcsc.com/product-detail/Current-Transformers_Qingxian-Zeming-Langxi-Elec-ZMPT101B_C111858.html)) | 1 | Galvanically isolated AC voltage sensor module suitable for bench validation and firmware calibration. Taps the building line voltage through screw terminals and outputs a scaled AC waveform at VOUT; the firmware's `volt_scale` env var accommodates any module producing a proportional output. See [§5](#5-wiring-and-assembly) for wiring details. **Replace with an agency-listed isolated voltage transducer before deploying to a commercial panel; see [§11](#11-limitations-and-next-steps) for the production path hardware and firmware requirements.** |
-| AC/DC supply, 5 V / 2 A (e.g. [MeanWell IRM-10-5](https://www.meanwell.com/Upload/PDF/IRM-10/IRM-10-SPEC.PDF)) | 1 | Derives 5 V DC from 120 VAC at a spare panel breaker. The 2 A rating provides headroom for the Notecard's cellular transmit burst on top of the host MCU and analog front-end circuits. |
-| [Molex 2091420180 Flexible LTE/Wi-Fi/GNSS Antenna](https://shop.blues.com/collections/accessories/products/flexible-cellular-or-wi-fi-antenna?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link), u.FL, 180 mm lead, 698 MHz–4.0 GHz | 1 | **Required** for any metal-enclosure or metal-panel installation — replace the stub antenna included with the Notecarrier CX kit if the 180 mm lead is insufficient to exit the enclosure via a cable gland. Route the antenna to **outside** the metal panel; see [§5](#5-wiring-and-assembly). For panels where the cable gland is further from the Notecard, add a u.FL-to-u.FL extension cable of appropriate length between the Notecard's antenna port and the antenna's u.FL terminator. |
-| NEMA 4X or panel-rated enclosure, ~6×4×2″ | 1 | Houses the Notecarrier CX, op-amp integrator board, and power supply in the panel room. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard MBGLW seats into the M.2 slot on the carrier. Mojo (bench-only) sits inline between the 5 V supply output and the Notecarrier CX `+VBAT` pad.
-
-
-
-**Cellular antenna — critical for metal-panel deployments.** Connect the external cellular antenna's u.FL lead to the Notecard's primary antenna port and route the cable through a cable gland to **outside the metal panel cabinet or NEMA enclosure**. Steel panels and enclosures act as Faraday cages; a rubber-duck or pigtail antenna left inside a closed metal panel will not establish a reliable cellular session. This is the single most common field bring-up failure in panel-mounted deployments.
-
-
-
-### Power chain
-
-```
-AC/DC 5 V supply (IRM-10-5)
- └─ +5 V out ──► Mojo BAT (bench validation — Mojo measures supply current)
- └─ Mojo LOAD ──► Notecarrier CX +VBAT (3.3 V regulated on-board)
-```
-
-For production deployment (no Mojo): connect the AC/DC supply 5 V output directly to Notecarrier CX `+VBAT`.
-
-
-
-**Do not connect the supply output simultaneously to both `+VBAT` and `+VUSB/+5V` on the Notecarrier CX.** The `+VBAT` pad is the correct power input for this supply voltage and load profile; feeding two supply rails at once risks back-feeding one regulator through the other.
-
-
-
-### Active Miller integrator (replicate for each of the four current channels)
-
-Each Rogowski coil outputs a voltage proportional to dI/dt (the derivative of the primary current). The active Miller integrator converts this dI/dt signal back to a signal proportional to instantaneous current, which can then be presented to the ADC for RMS and V×I computation.
-
-```
-Rogowski coil output (bare lead)
- │
- ├── R_in (10 kΩ) ──────────────────────────► op-amp IN− (inverting)
- │ │
- │ R_f (1 MΩ) │
- │ ┌────────────────────────────┤
- │ │ │
- │ │ C_f (10 nF) │
- │ └──┤├────────────────────────┘
- │
- │ op-amp IN+ (non-inverting) ──────────────── 1.65 V bias node
- │
- └── op-amp OUT ─────────────────────────────── to ADC pin (A0–A3)
-
-Bias node: +3V3 ── 100 kΩ ──┬── 100 kΩ ── GND
- ├── 10 µF ── GND
- └── op-amp IN+ of this channel
-```
-
-Use one section of the MCP6004 (U1A–U1D) per channel. All four op-amps share Vdd = +3V3 and GND.
-
-**Rogowski coil polarity:** if a loaded channel shows near-zero or zero `t*_wh` accumulation in Notehub despite a known load, the coil lead polarity may be reversed. The firmware clamps negative watts to zero before accumulating energy, so reversed polarity appears as zero/near-zero billed energy — not negative values — in Notehub. The negative-watt condition is visible on Serial (look for a negative watt figure in the `[sample] T*:` line) during bench bring-up. Swap the coil's two output leads at the R_in input of the integrator to correct polarity.
-
-### ZMPT101B voltage sensor module (bench/prototype — one module, building voltage reference)
-
-This design uses a **conditioned ZMPT101B module** (VCC / GND / VOUT pinout) — not a bare transformer secondary. The module's PCB carries the ZMPT101B transformer, an onboard burden resistor (sets the output scaling), and a calibration trim pot. Connect it as follows:
-
-```
-Building 120 V line ──► ZMPT101B module AC primary (IN1 / IN2 screw terminals,
- fused tap per local code)
-
-Module power and signal:
- VCC ──► +5V (from AC/DC supply before the Notecarrier CX regulator)
- GND ──► GND
-
- VOUT ──► 10 µF (series AC coupling) ──┬── ADC A4
- │
- +3V3 ── 100 kΩ ──────────┤
- │
- GND ── 100 kΩ ──────────┘
-```
-
-The module outputs an AC signal riding on a DC level set by its onboard circuitry (typically near half its supply voltage — approximately 2.5 V when powered at 5 V). VOUT is AC-coupled through the 10 µF series capacitor to ADC A4; the external 100 kΩ / 100 kΩ voltage divider then sets the DC operating point of the ADC input at 1.65 V, independent of the module's internal reference. Firmware removes any residual DC offset in software (`v_dc = mean(v_buf[])`), so the exact bias voltage does not need to be trimmed.
-
-The module's onboard burden resistor and trim pot set the output peak-to-peak swing. At 120 V primary with the factory burden, the output is typically ≈0.1 V RMS at the VOUT pin; `volt_scale` = 1200 (default) is the corresponding calibration constant (120 V ÷ 0.1 V). Measure with a precision AC voltmeter at a known line voltage and trim `volt_scale` in Notehub for best accuracy.
-
-### Pin-by-pin assignments
-
-- **A0** → Tenant 1 Rogowski integrator output (op-amp OUT direct)
-- **A1** → Tenant 2 Rogowski integrator output (op-amp OUT direct)
-- **A2** → Tenant 3 Rogowski integrator output (op-amp OUT direct)
-- **A3** → Tenant 4 Rogowski integrator output (op-amp OUT direct)
-- **A4** → ZMPT101B VOUT via 10 µF series AC coupling cap into the 1.65 V bias node
-- **+3V3** → MCP6004 Vdd, top of all half-rail bias dividers
-- **+5V** → ZMPT101B module VCC (bench)
-- **GND** → MCP6004 GND, bottom of all bias dividers, negative terminals of the four current-channel 10 µF bias-decoupling caps (A0–A3 bias nodes), Rogowski coil returns, ZMPT101B module GND *(the voltage-channel 10 µF capacitor is the series AC-coupling cap between ZMPT101B VOUT and A4 — its negative lead connects toward A4 and the 1.65 V bias node, not to GND; see the ZMPT101B wiring diagram above)*
-
-### Rogowski coil placement
-
-Each Rogowski coil wraps around **one conductor only** of the tenant's single-phase branch-circuit feed — the hot leg. The coil must close fully (the two ends of the flexible Rogowski coil clip together); an open coil gap produces a non-integrable signal. This firmware is designed for single-phase circuits where all tenants derive from the same phase leg; see [§11](#11-limitations-and-next-steps) for the multi-phase case.
-
-
-
-**Electrical safety.** Panel interiors operate at hazardous voltages. Installation must be performed by a licensed electrician following applicable electrical code, site lockout/tagout procedures, and the panel manufacturer's instructions. The Rogowski coils and voltage transducer are galvanically isolated from the mains; the hazard is the bus bars and termination points nearby during installation.
-
-
-
-
-Before powering on for the first time:
-
-- [ ] **PRODUCT_UID set** — edited `tenant_sub_meter_helpers.h` line 17 with your Notehub ProductUID (e.g., `com.your-company:your-app`).
-- [ ] **Core installed** — Arduino core for STM32 added via Boards Manager. Search "STM32 MCU based boards".
-- [ ] **Blues Notecard library installed** — installed via Library Manager.
-- [ ] **DIP switch set to HST** — on the Notecarrier CX, before connecting USB.
-- [ ] **Compiled and uploaded** — `arduino-cli compile` succeeds with no errors. `arduino-cli upload` completes successfully.
-- [ ] **Serial monitor at 115200 baud** — after upload, watch for `[init]` and `[sample]` lines (host is running).
-
-If compilation fails with "PRODUCT_UID is not defined", you missed step 1. If upload fails with "board not found", check the DIP switch and USB cable.
-
-## 6. Notehub Setup
-
-### Fast path to first event
-
-1. **Notehub** — create a [Notehub project](https://notehub.io), copy its ProductUID.
-2. **Wire the bench rig** — Notecarrier CX + Notecard MBGLW + four Rogowski integrator circuits on A0–A3 + ZMPT101B on A4. Full pinout in [§5](#5-wiring-and-assembly).
-3. **Edit one line** in [`firmware/tenant_sub_meter/tenant_sub_meter_helpers.h`](firmware/tenant_sub_meter/tenant_sub_meter_helpers.h) — replace `#define PRODUCT_UID ""` with your project's value.
-4. **Flash** —
- ```bash
- arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/tenant_sub_meter/
- arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/tenant_sub_meter/
- ```
- Full troubleshooting in [§7.1](#71-installing-and-flashing). Connect the Notecarrier CX with USB and set the DIP switch to `HST`.
-5. **Watch serial** — open the serial monitor at 115200 baud. You will see Notecard configuration, then channel sample lines. After the first sleep cycle, serial goes quiet (host is powered off) for 5 minutes — that silence is expected and healthy.
-6. **Check Notehub** — open **Events** tab. A `_session.qo` appears within minutes. The first `meter_summary.qo` typically arrives in the initial session, shortly after the first sample cycle completes (within an hour).
-
-### Fleets, environment variables, and routing
-
-Once your device is appearing in the **Devices** tab and events are flowing, configure the project for ongoing operation:
-
-1. **Create a Fleet per property.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. The natural unit here is *one fleet per building* — every panel in the same building typically operates at the same line voltage and uses the same Rogowski coil model, so fleet-level environment variables encode "this building's panels all use these sensor calibrations." Use [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) to auto-assign devices by serial number prefix for multi-property deployments.
-
-2. **Set environment variables.** Navigate in Notehub to **Fleet → Environment** (or **Device → Environment** for per-device overrides). Edit the table below, then save. The device pulls them on its next `inbound` sync (default: every 2 hours) — no reflash, no truck roll.
-
- | Variable | Default | Purpose | When to adjust |
- |---|---|---|---|
- | `sample_interval_sec` | `300` | Seconds between measurements (minimum 60, max 86400). | Leave at 300 for most installs; smaller values consume more power and produce more frequent events. |
- | `summary_interval_min` | `60` | Minutes between hourly summaries (minimum 15, max 1440). The minimum of 15 matches the 15-minute demand window. | Leave at 60 for standard hourly billing. Values < 15 are silently clamped to 15. |
- | `rogowski_amps_per_volt` | `400.0` | Rogowski coil + integrator sensitivity in A/V. Derivation: rated primary amps ÷ integrator output V RMS at rated load. | **Critical**: Calibrate against a reference clamp meter at a known full load before billing. Incorrect values produce proportionally wrong energy readings. See [§10](#10-validation-and-testing) calibration note. |
- | `volt_scale` | `1200.0` | Voltage transducer calibration: line-voltage V RMS ÷ ADC-pin V RMS. | Default (1200.0) matches ZMPT101B at 120 V nominal. Measure line voltage with a precision AC voltmeter and trim for accuracy. |
- | `num_tenants` | `4` | Active current channels to sample (1–4). | Set to the actual number of tenants metered to skip unnecessary ADC reads. |
-
-3. **Configure routes.** Route `meter_summary.qo` to your time-series database or billing platform. This is the canonical energy record and the correct source for monthly aggregation — sum each tenant's `t*_wh` values across all hourly events over the billing period using the [Notehub Event Query API](https://dev.blues.io/api-reference/notehub-api/). See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for destination types.
-
-### What you should see in Notehub
-
-Within minutes of first power-on, the **Events** tab in your project should show a `_session.qo` (the cellular handshake). The firmware emits a summary note on first boot, so the first `meter_summary.qo` typically arrives in the initial session.
-
-**Example `meter_summary.qo` payload:**
-
-```json
-{
- "t1_wh": 482.3,
- "t1_demand_w": 3120.0,
- "t2_wh": 214.1,
- "t2_demand_w": 1480.5,
- "t3_wh": 673.8,
- "t3_demand_w": 4220.0,
- "t4_wh": 149.2,
- "t4_demand_w": 980.0,
- "fault_mask": 0
-}
-```
-
-This arrives hourly. `t*_wh` is estimated interval energy in watt-hours per tenant over the past hour; sum these across all hourly events over a billing period to derive monthly kWh. `t*_demand_w` is the peak 15-minute average demand (in watts) in that hour. `fault_mask = 0` means all channels passed hardware health checks.
-
-## 7. Firmware Design
-
-Three-file implementation — all three must be present in the same sketch directory:
-
-- [`firmware/tenant_sub_meter/tenant_sub_meter.ino`](firmware/tenant_sub_meter/tenant_sub_meter.ino) — main sketch: `setup()` / `loop()` / `runCycle()`, state management, measurement orchestration, and sleep/wake lifecycle.
-- [`firmware/tenant_sub_meter/tenant_sub_meter_helpers.h`](firmware/tenant_sub_meter/tenant_sub_meter_helpers.h) — shared types (`TenantState`, `PersistState`, `RuntimeConfig`, `ChannelMeasurement`), constants, fault-flag definitions, and helper function declarations.
-- [`firmware/tenant_sub_meter/tenant_sub_meter_helpers.cpp`](firmware/tenant_sub_meter/tenant_sub_meter_helpers.cpp) — implementations of all helper functions (`measureChannel`, `notecardReady`, `fetchEnvOverrides`, `initNotecard`, `defineTemplates`, `sendSummary`, and `getEpochSec`).
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Install via the Arduino Boards Manager (search "STM32 MCU based boards") or add the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**. Select **Blues Cygnet** as the board target (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-- **`Blues Wireless Notecard`** library — [`note-arduino`](https://github.com/blues/note-arduino). Install via the Arduino Library Manager, or `arduino-cli lib install "Blues Wireless Notecard"`. Verify the latest release against the [note-arduino releases page](https://github.com/blues/note-arduino/releases) at install time.
-
-**Flashing — Arduino IDE:** Open `tenant_sub_meter.ino`, select the Cygnet board, and click **Upload**. The Notecarrier CX exposes the ST-Link interface on the same USB cable — no external programmer needed. Set the DIP switch to `HST` before flashing to route USB serial to the host MCU.
-
-**Flashing — `arduino-cli` (step by step):**
-
-First, confirm the board and port:
-
-```bash
-# List all recognized boards
-arduino-cli board listall | grep -i cygnet
-# Expected output: STMicroelectronics:stm32:Blues:pnum=CYGNET
-```
-
-Then set the DIP switch on the Notecarrier CX to `HST` and compile/upload:
-
-```bash
-# Compile only (no upload yet)
-arduino-cli compile \
- -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- firmware/tenant_sub_meter/
-
-# List USB ports connected to see your device
-arduino-cli board list
-
-# Upload (replace /dev/cu.usbmodem* with your actual port from board list)
-arduino-cli upload \
- -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/cu.usbmodem* \
- firmware/tenant_sub_meter/
-
-# Watch the progress — when upload is complete, disconnect USB and flip DIP to NCD
-```
-
-On Windows, ports are named `COM3`, `COM4`, etc.; on macOS / Linux, `/dev/cu.usbmodem*` or `/dev/ttyUSB*`.
-
-After flashing, open the serial monitor at **115200 baud**. On first power-on you will see the Notecard configuration exchange, then the first channel sample lines. After the first sleep, the serial output goes quiet for `sample_interval_sec` — that silence is expected; the host is fully powered off and will re-enter `setup()` from cold on the next wake.
-
-### 7.2 Module map
-
-| Responsibility | Where |
-|---|---|
-| Notecard cold-boot readiness handshake (first I2C transaction on every wake) | `notecardReady()` |
-| Notecard init, `hub.set`, accelerometer disable | `initNotecard()` |
-| Template registration (`meter_summary.qo` only) | `defineTemplates()` |
-| Environment variable fetch and clamping | `fetchEnvOverrides()` |
-| Sequential-interleaved V×I measurement, RMS current and active power | `measureChannel()` |
-| Wh accumulation, demand-window accumulation and peak demand tracking | `runCycle()` sample loop |
-| Hourly summary emission with confirmed note.add | `sendSummary()` |
-| State persist + host sleep | `NotePayloadSaveAndSleep()` |
-| State restore on wake (after notecardReady) | `NotePayloadRetrieveAfterSleep()` |
-
-### 7.3 Sensor reading strategy
-
-The `measureChannel()` function captures 2000 interleaved voltage/current sample pairs from VOLTAGE_PIN (voltage transducer output) and the selected current pin (Rogowski integrator output). At the STM32L433's default 12-bit ADC rate, each `analogRead()` takes approximately 50 µs; one interleaved pair therefore takes ~100 µs, so 2000 pairs cover ~200 ms, approximately 12 full 60 Hz cycles. Voltage is sampled first within each pair, so any systematic ADC switching latency between channels is consistent across all pairs.
-
-**DC removal.** Both signals are biased to 1.65 V (half-rail) by external resistor dividers. A single-pass mean calculation over all samples determines the per-channel DC offset, which is subtracted before computing RMS or cross-products. This removes the bias voltage and any residual ADC offset without a separate hardware AC-coupling capacitor in the signal path.
-
-**Active power computation.** Active power P = mean(v[n] × i[n]), where v[n] and i[n] are the DC-removed voltage and current samples scaled to physical units. The V×I cross-product captures the actual phase relationship between voltage and current; no assumed power factor is needed.
-
-**Phase/accuracy limitation from sequential sampling.** Because voltage and current are read sequentially (not simultaneously), there is a fixed ~50 µs time offset between the V sample and the I sample in each pair. At 60 Hz, one full cycle is 16.67 ms, so 50 µs corresponds to a consistent phase offset δ of approximately 1.08°. The relative error in active power is approximately tan(φ)·sin(δ) — where φ = arccos(PF) is the load power-factor angle — because the measured cross-product evaluates cos(φ − δ) rather than cos(φ). At power factors above 0.9 (typical commercial loads, φ ≈ 26°), this error is approximately 1% of the true active-power reading. At PF 0.7 (φ ≈ 46°) it reaches approximately 2%. For inductive loads (positive φ) the firmware systematically over-reads active power by this margin. For metering applications requiring certified accuracy (e.g., utility-tariff sub-metering subject to accuracy regulations), a dedicated simultaneous-sampling energy-metering IC should replace this approach.
-
-A Rogowski coil installed with reversed lead polarity produces a consistently negative watts result; the firmware clamps negative values to zero and the issue is visible in the serial log as a zero-energy reading on the affected channel.
-
-**Energy estimation.** Each `sample_interval_sec` (default 5 min) the firmware takes one ~200 ms active-power snapshot (mean V×I over 2000 sample pairs) and multiplies it by the full interval duration to produce an estimated interval energy in Wh. This is accurate for constant or slowly-varying loads, but may over- or under-state energy on bursting or cycling loads where power changes significantly within the 5-minute window. The `t*_wh` accumulator sums these interval estimates; it is not a continuous integral. This makes the design suitable for proportional load allocation (identifying which tenants use more energy than others) but not for applications requiring certified metering accuracy.
-
-**Demand window.** Energy accumulated in `demand_window_mwh` and elapsed time in `demand_window_sec` grow across sample wakes. Once `demand_window_sec` reaches `DEMAND_INTERVAL_SEC` (900 s = 15 minutes), average watts are computed as `demand_window_mwh × 3600 / (demand_window_sec × 1000)` and compared against `peak_demand_cw`, the per-tenant centi-watt accumulator for the current summary period. If the new average is higher, `peak_demand_cw` is updated. Both window fields then reset to begin the next 15-minute interval. This is a true interval-average demand reading — not a transient snapshot — matching the most common North American utility demand-charge billing window. The `t*_demand_w` field in `meter_summary.qo` reports `peak_demand_cw / 100` (converted to watts) for the summary period. The demand window deliberately straddles summary-period boundaries: `peak_demand_cw` is cleared after each confirmed `sendSummary()`, but `demand_window_mwh` and `demand_window_sec` continue uninterrupted so no 15-minute interval is silently truncated at a summary boundary.
-
-**Summary interval and demand window floor.** `summary_interval_min` is clamped to a minimum of 15 in firmware (`MIN_SUMMARY_INTERVAL_MIN`), matching `DEMAND_INTERVAL_SEC / 60`. A summary period shorter than one demand window would always emit `t*_demand_w = 0` because no complete 15-minute window could close within it; this is not a hardware fault, just a window-timing gap. Operators who need finer-grained reporting should use the `t*_wh` field and accumulate their own demand windows in the downstream system.
-
-**Per-channel fault bitmask.** Each call to `measureChannel()` checks four conditions per channel nibble: `FAULT_BIAS_RANGE` (0x01) when the ADC DC offset falls outside the expected 1.40–1.90 V half-rail window; `FAULT_SATURATED` (0x02) when the centered signal's RMS exceeds the RMS-equivalent of 85 % of the 1.65 V half-rail peak amplitude (≈ 0.99 V RMS at the ADC pin), indicating that the waveform is approaching ADC rail clipping; bit 2 reserved (formerly `FAULT_NO_SIGNAL`, retired because a legitimately unloaded tenant circuit is indistinguishable from a disconnected Rogowski coil by current magnitude alone; see the commissioning diagnostic note below); and `FAULT_VOLTAGE_REF` (0x08) when the shared voltage reference path is suspect (bias out of range, signal saturated, or measured line RMS below `VOLTAGE_MIN_V_RMS`). Because the voltage reference is shared, `FAULT_VOLTAGE_REF` is propagated into every active channel's fault nibble simultaneously so downstream billing can identify periods where all tenant watt calculations are compromised. Fault flags are OR'd across all sample wakes in the summary period and packed into the `fault_mask` field of `meter_summary.qo` as a 4-bit nibble per tenant channel (T1 in bits 3:0, T4 in bits 15:12). A `fault_mask` of 0 means all channels passed all checks on every sample in the period. Downstream billing systems should reject or flag any summary where `fault_mask != 0`. Low RMS current (below 0.05 A) is logged to Serial as a commissioning diagnostic only; it is never placed in `fault_mask`.
-
-### 7.4 Event payload design
-
-`meter_summary.qo` is registered as a [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) notefile at first boot. Templates store Notes as fixed-length binary records, reducing on-wire size by 3–5× vs. free-form JSON.
-
-**`meter_summary.qo` — emitted hourly (canonical record)**
-
-Nine 4-byte floats: estimated interval energy (Wh), 15-minute blocked-average demand (W), and a combined fault bitmask per summary period. `t*_wh` is the sum of (W_sample × sample_interval_sec / 3600) across all periodic samples in the reporting period: estimated interval energy derived from power snapshots, not a continuous integral or kVAh estimate. `t*_demand_w` is the peak 15-minute blocked-average demand observed during the summary period: the highest average watts across any completed `DEMAND_INTERVAL_SEC` (900 s) window; see §7.3. `fault_mask` packs per-channel hardware-fault flags as a 4-bit nibble per tenant (T1 in bits 3:0; see §7.3 for bit definitions); 0 means all channels clean for the entire period.
-
-```json
-{
- "t1_wh": 482.3,
- "t1_demand_w": 3120.0,
- "t2_wh": 214.1,
- "t2_demand_w": 1480.5,
- "t3_wh": 673.8,
- "t3_demand_w": 4220.0,
- "t4_wh": 149.2,
- "t4_demand_w": 980.0,
- "fault_mask": 0
-}
-```
-
-To derive monthly per-tenant totals, sum `t*_wh` across all `meter_summary.qo` events for a device over the billing period using the [Notehub Event Query API](https://dev.blues.io/api-reference/notehub-api/), then divide by 1000 to obtain estimated kWh. This Notehub-side aggregation is the sole monthly rollup path; no device-side monthly note is generated.
-
-### 7.5 Low-power strategy
-
-The panel installation is line-powered, but keeping the host asleep between samples reduces enclosure heat, reduces supply wear, and produces firmware that ports directly to battery variants without a rewrite. After each sample cycle, `NotePayloadSaveAndSleep` serializes the RAM `PersistState` struct into Notecard flash and issues a [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) request that cuts host power for `sample_interval_sec` seconds. The Notecarrier CX's ATTN→EN routing handles the physical power switch; no external relay or MOSFET is needed. The Cygnet re-enters `setup()` from cold on each wake; `NotePayloadRetrieveAfterSleep` rehydrates the struct transparently, but only after `notecardReady()` has confirmed the Notecard is ready to accept I2C requests.
-
-The Notecard sits in its own [low-power idle state](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) (~18 µA at 5 V for the Cell+WiFi variant) between cellular sessions. Summary notes queue locally and flush together in one hourly session; the radio is not touched on intermediate wakes.
-
-### 7.6 Retry and error handling
-
-**Cold-boot I²C race.** On every cold boot, `notecardReady(NOTECARD_READY_TIMEOUT_SEC)` is the very first Notecard transaction; it precedes `NotePayloadRetrieveAfterSleep` and every other I2C call. `notecardReady` calls `notecard.sendRequestWithRetry(req, 10)` with a lightweight `card.version` request. Per the [note-arduino library docs](https://dev.blues.io/tools-and-sdks/firmware-libraries/arduino-library/), `sendRequestWithRetry` returns `bool` (true = acknowledged) — it does **not** return a `J*` response pointer — and blocks up to the specified timeout. This handles the race condition where the STM32L433 host comes up several hundred milliseconds before the Notecard is ready to accept I²C requests.
-
-If `notecardReady()` returns `false` (genuine hardware or I²C fault), the firmware does **not** attempt any further I²C transaction, including `NotePayloadRetrieveAfterSleep` and `NotePayloadSaveAndSleep`. A plain `delay(DEFAULT_SAMPLE_INTERVAL_SEC)` allows `loop()` to retry on bench rigs without ATTN→EN wiring; on a production Notecarrier CX, the Notecard will restore host power when it recovers. All subsequent transactions, including `fetchEnvOverrides()` (`env.get`) and `hub.set` in `initNotecard()`, are issued only after a successful handshake.
-
-**Environment variable fetch failure.** `fetchEnvOverrides()` calls `env.get` and checks for a `NULL` response (Notecard not yet synced) before touching the `RuntimeConfig` struct. If the response is absent or malformed, firmware defaults are kept intact. Numeric bounds checks guard each variable; for example, `rogowski_amps_per_volt` is only applied if it falls in (0, 10000], preventing a zero or negative value from corrupting subsequent power calculations.
-
-**`card.time` not yet valid.** `getEpochSec()` returns 0 if the Notecard has not yet established a time sync. The `first_summary_sent` latch prevents the firmware from re-triggering `sendSummary()` on every pre-sync wake: once `sendSummary()` succeeds, the latch is set and subsequent triggers require `now > 0` and an elapsed `summary_interval_min`. If the very first `sendSummary()` succeeded while `now` was still 0, `last_summary_epoch` is left at 0, which would permanently block subsequent hourly summaries. On the first wake where `now > 0`, the firmware detects this condition (`first_summary_sent && last_summary_epoch == 0`) and seeds `last_summary_epoch = now` so the normal elapsed-time gate takes over from that point.
-
-**`note.add` / template failure: the most critical failure path.** `sendSummary()` is gated on `state.notecard_configured == 1`. If `initNotecard()` or `defineTemplates()` fails on a given wake, the configured flag stays clear, all note emission is skipped for that cycle, and accumulators are preserved without ever reaching the send path. When `notecard_configured` is set, `sendSummary()` uses `notecard.requestAndResponse()` rather than the fire-and-forget `sendRequest()`, so the Notecard's acknowledgement (or error) is explicitly checked:
-
-```cpp
-J *rsp = notecard.requestAndResponse(req);
-bool ok = rsp && !notecard.responseError(rsp);
-notecard.deleteResponse(rsp);
-return ok;
-```
-
-**Accumulators are only reset if `sendSummary()` returns `true`.** If the Notecard returns an error (queue full, I²C fault, or any other condition), the per-tenant `accum_wh_milli` and `peak_demand_cw` fields are preserved in persistent state. On the next wake cycle the firmware retries `sendSummary()` with the accumulated totals, which now include an additional sample period's energy. This retry loop continues until a successful acknowledgement is received. Because state is serialized to Notecard flash between wakes, the data survives a power cycle during the retry window.
-
-**Template registration failure.** `defineTemplates()` uses `requestAndResponse()` and returns `false` if the Notecard returns an error. The `notecard_configured` flag is only set to 1 if both `initNotecard()` and `defineTemplates()` return `true`; a failure leaves the flag clear so the next wake automatically retries the full `initNotecard()` + `defineTemplates()` sequence. Because all note emission is gated on `notecard_configured`, no Note can be queued before the template exists; the firmware defers emission until setup completes, accumulating energy across the retry wakes without loss. If template registration is suspected to have failed after initial deployment (e.g., after a factory reset of the Notecard), the deterministic recovery procedure is:
-
-1. Wait for any pending notes to complete an outbound sync (so queued data is not lost).
-2. Issue `{"req":"card.restore","mode":"factory"}` via the [Notecard In-Browser Terminal](https://dev.blues.io/terminal/) or the USB serial REPL. This resets the Notecard to factory defaults and clears its stored NotePayload, including the `notecard_configured` flag.
-3. Power-cycle the device. On the next boot, `notecard_configured` is 0, so the firmware runs the full `initNotecard()` + `defineTemplates()` sequence again.
-
-
-
-`card.restore` with `mode:"factory"` also clears any Notes queued in the Notecard's local store. Complete step 1 before restoring if preserving in-flight data is important.
-
-
-
-### 7.7 Key code snippet 1: template registration
-
-Only `meter_summary.qo` is registered as a binary-packed template. `14.1` is the note-c type hint for a 4-byte IEEE-754 float (TFLOAT32). `defineTemplates()` uses `requestAndResponse()` and returns a `bool`; `notecard_configured` is only latched after a successful response.
-
-```cpp
-bool defineTemplates(void) {
- J *req = notecard.newRequest("note.template");
- if (!req) return false;
- JAddStringToObject(req, "file", SUMMARY_NOTEFILE);
- J *body = JAddObjectToObject(req, "body");
- char key[28];
- for (int t = 1; t <= 4; t++) {
- snprintf(key, sizeof(key), "t%d_wh", t);
- JAddNumberToObject(body, key, 14.1); // estimated interval energy, TFLOAT32
- snprintf(key, sizeof(key), "t%d_demand_w", t);
- JAddNumberToObject(body, key, 14.1); // peak 15-min demand-window average, TFLOAT32
- }
- // fault_mask: 4-bit nibble per channel, packed uint16 as TFLOAT32.
- // 0 = all channels clean; non-zero = at least one FAULT_* flag set.
- JAddNumberToObject(body, "fault_mask", 14.1);
- J *rsp = notecard.requestAndResponse(req);
- bool ok = rsp && !notecard.responseError(rsp);
- notecard.deleteResponse(rsp);
- return ok;
-}
-```
-
-### 7.8 Key code snippet 2: sleep with state persistence
-
-`NotePayloadSaveAndSleep` serializes the RAM struct into Notecard flash, then triggers the ATTN-based host power cutoff. The next `setup()` call opens with `notecardReady()` (the cold-boot handshake that must be the first I2C transaction) followed by `NotePayloadRetrieveAfterSleep`, which reconstructs the struct exactly as it was left: accumulated estimated Wh, peak demand, demand-window progress, and summary epoch all intact across the power cycle.
-
-```cpp
-NotePayloadDesc out = {0};
-NotePayloadAddSegment(&out, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&out, cfg.sample_interval_sec, NULL);
-```
-
-## 8. Build and Flash
-
-**Prerequisites:** Arduino IDE (or `arduino-cli` on the command line) with the following installed via Boards Manager and Library Manager:
-- Boards: `STM32 MCU based boards` (the `stm32duino/Arduino_Core_STM32` core). Add the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs** if it is not already listed.
-- Libraries: `Blues Wireless Notecard` (from [`note-arduino`](https://github.com/blues/note-arduino)).
-
-**Steps:**
-
-1. Clone or download the repo and open [`firmware/tenant_sub_meter/tenant_sub_meter.ino`](firmware/tenant_sub_meter/tenant_sub_meter.ino) in the Arduino IDE. All three files in the `firmware/tenant_sub_meter/` directory must be present in the same sketch folder.
-
-2. Set your Notehub ProductUID in [`firmware/tenant_sub_meter/tenant_sub_meter_helpers.h`](firmware/tenant_sub_meter/tenant_sub_meter_helpers.h) by replacing the empty string on line 17:
- ```cpp
- #define PRODUCT_UID "com.your-company:your-app"
- ```
- Find this value in Notehub under **Project Settings → ProductUID**, or see [Finding a ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid).
-
-3. Set the DIP switch on the Notecarrier CX to `HST` so USB serial routes to the host MCU for programming.
-
-4. **Via Arduino IDE:** Select **Tools → Board → Blues Cygnet**, choose the serial port that appears when the Notecarrier CX is connected over USB, then click **Upload**. The Notecarrier CX exposes the ST-Link interface on the same USB cable, so no external programmer is needed.
-
-5. **Via command line (`arduino-cli`):**
- ```bash
- # Compile
- arduino-cli compile \
- -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- firmware/tenant_sub_meter/
-
- # Find the port (replace below with the value reported)
- arduino-cli board list
-
- # Upload
- arduino-cli upload \
- -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/cu.usbmodem* \
- firmware/tenant_sub_meter/
- ```
- On Windows the port appears as `COM3`, `COM4`, etc.; on macOS or Linux it is typically `/dev/cu.usbmodem*` or `/dev/ttyUSB*`.
-
-6. Open the Arduino IDE Serial Monitor at **115200 baud** to verify the sketch boots. On first power-on you should see the Notecard configuration exchange (`hub.set`, template registration), followed by the first channel sample lines. After the initial cycle the host enters cold sleep for `sample_interval_sec` and the serial output goes quiet — that silence is expected, since the Notecarrier CX cuts host power between wakes and `setup()` runs again from scratch on the next sample.
-
-7. Once the device appears in Notehub and the first `meter_summary.qo` Note arrives, flip the DIP switch back to `NCD` for production operation.
-
-If compilation fails with a `PRODUCT_UID is not defined` warning, revisit step 2. If upload fails with a "board not found" error, check the DIP switch position and USB cable.
-
-## 9. Data Flow
-
-
-
-**Collected.** Every `sample_interval_sec` (default 5 minutes): 2000 sequential interleaved V+I ADC sample pairs per active tenant channel. The V×I cross-product yields active power (W); RMS current (A) is computed as an intermediate value and is available to callers but is not transmitted in the current firmware. Adding per-channel RMS current to the summary Note is a straightforward future extension. See [§11](#11-limitations-and-next-steps).
-
-**Accumulated on-device.** Between summaries: per-tenant estimated Wh (sum of W_sample × sample_interval_sec / 3600); per-tenant peak 15-minute blocked-average demand W (highest completed `DEMAND_INTERVAL_SEC` window average; see §7.3); per-tenant hardware-fault flags (OR of FAULT_* bits across all wakes in the period).
-
-**Transmitted — hourly (canonical record).**
-`meter_summary.qo`: one Note per `summary_interval_min` (default 24 per day). Template-encoded, queued and flushed in the Notecard's periodic outbound sync. Carries `t1_wh`–`t4_wh` (estimated interval energy in Wh), `t1_demand_w`–`t4_demand_w` (peak 15-minute blocked-average demand W), and `fault_mask` (combined per-channel fault bitmask) for each tenant. Note is **not** sync:true; it batches with the periodic radio window. These Notes are the authoritative energy record and the sole input for Notehub-side monthly aggregation.
-
-**Routed.** Notes flow through Notehub to whatever downstream the project routes specify. A typical deployment routes `meter_summary.qo` to a time-series database as the primary billing and analytics record. Monthly per-tenant totals are derived by summing `t*_wh` across the billing period's hourly events in Notehub — either via the [Event Query API](https://dev.blues.io/api-reference/notehub-api/) or a downstream aggregation pipeline. Notehub stores every Note durably; no device-side aggregate is needed to reconstruct any monthly total.
-
-## 10. Validation and Testing
-
-**Expected cadence in steady state.** A correctly-operating unit generates one `meter_summary.qo` event per hour. The first `_session.qo` should appear in Notehub within two minutes of first power-on; the first `meter_summary.qo` typically arrives in the initial session shortly after the first sample cycle completes.
-
-**Calibration — before deployment.** Each Rogowski coil and integrator combination has a unique sensitivity. Before using readings for billing:
-
-1. Load one tenant circuit to a known stable draw (e.g., 10 A measured by a calibrated clamp meter).
-2. Wait one hour for a `meter_summary.qo` to arrive in Notehub.
-3. Calculate expected energy: `loaded_amps × hours_run × 120_V = Wh_expected`. For 10 A @ 1 hour at 120 V ≈ 1200 Wh.
-4. Compare `t*_wh` in that hour's summary against expected. The ratio is your per-coil correction factor.
-5. Update `rogowski_amps_per_volt` in Notehub Fleet Environment: `(default_400 × expected_wh) / reported_wh`. Push the change; device fetches it on next inbound sync (default: 2 hours).
-6. Repeat for a second load point (e.g., 20 A) to confirm linearity. Repeat for each active tenant channel.
-
-Skipping calibration leaves you with readings that may be off by ±10–30% depending on coil model and integrator tolerance.
-
-**Using Mojo to validate power behavior.** The Blues Mojo coulomb counter connects inline between the 5 V supply output and the Notecarrier CX `+VBAT` input and reports cumulative mAh to the Notecard over its Qwiic connector. Published current figures from the [Notecard low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) and the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/):
-
-| Phase | Expected current |
-|---|---|
-| Notecard idle (between syncs, radio off) | ~18 µA @ 5 V (Cell+WiFi variant) |
-| Host active (sampling four channels) | ~15–30 mA for < 1 seconds |
-| Notecard cellular session (LTE Cat-1 bis) | ~0.28 mAh per warm sync; ~0.49 mAh after cold restart; depends on signal strength |
-| Host off (card.attn sleep, between wakes) | essentially 0 mA from +VBAT |
-
-The dominant energy cost is the once-per-hour cellular session. Because the supply is line-powered, absolute mAh is less important than the *shape* of the current trace:
-
-- **Healthy trace:** near-zero baseline, brief (<1 seconds) micro-spikes every 5 minutes as the host wakes and samples, one 10–20 seconds burst at 80–150 mA once per hour.
-- **Host not sleeping:** continuous 15–30 mA baseline. Almost always a `card.attn` routing fault; verify the ATTN pin is driving the EN rail on the Notecarrier CX.
-- **Cellular sessions running long (60+ s):** correctly-spaced hourly bursts but each running long. Usually a weak-signal site; check antenna placement or move closer to a window.
-
-### Troubleshooting
-
-| Symptom | Likely cause | What to check |
-|---|---|---|
-| Device never appears in Notehub **Devices** tab. | `PRODUCT_UID` empty or wrong; cellular antenna disconnected; poor cellular coverage. | Re-verify `PRODUCT_UID` in firmware matches Notehub project ProductUID exactly. Check serial monitor for `[init] hub.set failed` messages (firmware can't reach Notehub). Move unit near a window or outside. |
-| `_session.qo` appears but no `meter_summary.qo` after 90+ minutes. | Template registration failed; or Note reaches Notehub but no route is configured. Zero-load channels still produce events. | Check Notehub **Events** tab directly — `meter_summary.qo` appears there regardless of routes. Watch serial for `[summary] note.add failed` messages. If template failed, restart firmware (cold power-cycle device). |
-| Serial monitor shows `[sample] T*: negative watts` or `[sample] T*: 0 watts` under known load. | Rogowski coil leads reversed (polarity inverted). | Swap the coil's two output leads at the R_in resistor input on the integrator board and re-test. |
-| One channel reads large values, another reads zero, while under identical load. | Coil not fully closed (gap in the flexible snap coupler); or integrator component failure. | Inspect the affected coil snap-coupler for a gap. Confirm all four integrator resistor values (R_in, R_f) and capacitor (C_f) match the values in [§5](#5-wiring-and-assembly). |
-| All four channels read the same non-zero value even when unloaded. | Cross-talk or bias-node coupling issue; inadequate decoupling on shared 3.3 V supply. | Verify each of the five bias nodes (four current + one voltage) has its own dedicated 10 µF electrolytic cap to GND, placed as close as possible to the node. Check the shared `+3V3` rail for low impedance using a voltmeter under load. |
-| `t*_wh` values are implausibly large or small (off by 10%+ vs. clamp meter). | Calibration constant `rogowski_amps_per_volt` is wrong for the installed coil/integrator. | Follow the calibration procedure in [§10](#10-validation-and-testing) above. Measure a known load with a calibrated clamp meter, wait for one hourly summary, and compute the correction factor. |
-| Mojo bench trace shows continuous 15–30 mA instead of sleep pulses. | Host not sleeping — ATTN is not gating power to EN. | Confirm you are using a Notecarrier CX, which routes ATTN→EN internally. On a Notecarrier F or raw breakout board, ATTN must be wired externally to the EN pin. |
-| Cellular session bursts occur every 5 minutes instead of once per hour. | `hub.set` not persisted correctly; or Notecard reverted to defaults. | Watch serial for `[init] hub.set` on each wake. If present, the config is not sticking. Restart the Notecard: issue `{"req":"card.restore","mode":"factory"}` in the [Notecard In-Browser Terminal](https://dev.blues.io/terminal/), then power-cycle the device and reflash firmware. |
-
-If a problem is not on this list, visit the [Blues community forum](https://discuss.blues.com); it's the fastest path to a second pair of eyes on a specific Notecard + sensor bring-up issue.
-
-## 11. Limitations and Next Steps
-
-This is a reference design for proportional tenant allocation, not a certified utility sub-meter, and that distinction is deliberate. The list below makes explicit where the design's measurement model, the agency-listing of the bench-grade voltage transducer, and the single-phase assumption draw the line, then points at the production paths for the cases that need more.
-
-### Simplified for this Proof-of-Concept
-
-The simplifications below are deliberate scope choices; each marks where the measurement model, the bench-grade voltage transducer, or the single-phase assumption draws the line for a proportional allocation bridge rather than a certified meter.
-
-**Allocation-grade estimation, not certified metering.** This design measures estimated interval energy by taking one ~200 milliseconds active-power snapshot per `sample_interval_sec` and multiplying it by the full interval duration. The firmware does not continuously integrate power between wakes. This approach is accurate for constant or slowly-varying loads but may over- or under-state energy on bursting or cycling loads. For internal bill-back between tenants in the same building it is an appropriate allocation method. It is **not** suitable for applications where accuracy is subject to regulatory oversight, for example, utility-tariff sub-metering subject to accuracy standards — without replacing the measurement front end with a dedicated simultaneous-sampling energy-metering IC or a certified pulse-output sub-meter interface.
-
-**Single-phase voltage reference shared across all channels.** The voltage transducer provides one voltage waveform used as the reference for all four current channels. This is accurate when all tenant circuits derive from the same phase leg of the building supply — common in small commercial buildings with a single-phase 120 V service. In a split-phase (120/240 V) building where tenants may be on different legs, or in a three-phase building where tenant feeds come from different phases, the phase relationship between the shared voltage reference and a tenant's actual line voltage introduces a power-factor error in the real-power calculation. For buildings with known multi-phase distribution, a dedicated voltage sensor per phase (and per-phase V×I pairs in the firmware) is required for accurate real-power metering across all tenants.
-
-**Rogowski coil and integrator calibration.** The `rogowski_amps_per_volt` default is a starting point derived from the integrator component values and a typical coil's mutual inductance; the actual value varies by coil model, production tolerance, and board layout. Calibration against a reference clamp meter at a known load is required before the readings are used for billing. The `volt_scale` default likewise assumes the ZMPT101B module's standard burden; different burden resistor values require a different constant. Both are tunable via Notehub environment variables without a reflash.
-
-**`t*_demand_w` matches the 15-minute demand-charge window but energy is still estimated.** The `t*_demand_w` field reports the peak 15-minute blocked-average demand (average watts over the highest completed 900-second window in the summary period). This matches the most common North American utility demand-charge billing interval and is a useful load-profile indicator and a reasonable input for internal bill-back proportioning. However, the underlying energy is still derived from periodic power snapshots, not a continuous integral. For 30-minute demand tariffs, change `DEMAND_INTERVAL_SEC` to `1800` in the firmware and reflash.
-
-**Runtime sensor-fault detection is best-effort, not certified.** `fault_mask` currently reflects three categories of hardware condition: `FAULT_BIAS_RANGE` (ADC DC offset outside the expected 1.40–1.90 V half-rail window), `FAULT_SATURATED` (centered-signal RMS approaching the ADC rail, trips when RMS exceeds the equivalent of 85 % of the 1.65 V half-rail peak amplitude, ≈ 0.99 V RMS at the ADC pin), and `FAULT_VOLTAGE_REF` (shared voltage-reference path bias, saturation, or implausibly low line voltage, propagated simultaneously into every active channel's nibble). Low or absent current is intentionally excluded from `fault_mask`: a legitimately unloaded tenant circuit produces the same near-zero current reading as a disconnected Rogowski coil, so treating it as a hard fault would quarantine valid zero-usage billing intervals. These checks can catch gross hardware faults but cannot distinguish a genuinely zero-load tenant from a silently failed sensor. A production deployment that requires more rigorous fault coverage should add a commissioning-time baseline capture and cross-check `fault_mask` against historical baselines in the downstream system.
-
-**Single-phase circuits only.** Each Rogowski coil wraps one conductor. A split-phase (120/240 V) tenant feed requires two coils per tenant; a three-phase feed requires three. This design uses A0–A4, leaving one remaining on-board analog input (A5) before an external analog multiplexer or ADC is required.
-
-**Electrical safety and code compliance.** This design is a proof-of-concept. Panel installations in commercial buildings must comply with applicable electrical codes (NEC in the United States, CEC in Canada, etc.), be performed by licensed electricians, and in some jurisdictions require a permit. Sub-metering equipment used for tenant billing may also be subject to accuracy and certification requirements under local utility or metering regulations. Confirm local requirements before deploying.
-
-**No local display.** The firmware provides no real-time readout at the panel. A small OLED on I²C or a status LED driven by a firmware threshold check would be a straightforward addition.
-
-**Mojo is bench equipment.** The firmware does not read the Mojo's coulomb-counter register over Qwiic. Adding a runtime mAh field to the summary Note is a straightforward extension if fleet-level power telemetry is valuable.
-
-### Production Next Steps
-
-Taking the bridge toward a production rollout means calibrating each installed coil, replacing the bench-grade voltage front end with an agency-listed transducer, and wiring the hourly stream into a billing platform. The following extensions are the natural progression.
-
-**Per-channel calibration workflow** comes first: at commissioning, capture a reference reading alongside a calibrated clamp meter to establish the correct `rogowski_amps_per_volt` for each installed coil, then push the trimmed value as a per-device environment variable in Notehub.
-
-**Production voltage sensing (future revision).** The ZMPT101B module used in this design is bench/prototype only; it is **not agency-listed** for permanent installation inside a commercial panel enclosure. Before deploying to a live panel, replace it with an agency-listed isolated AC voltage transducer. Two substitution paths are available depending on the device chosen:
-
- **(a) AC waveform output** (e.g., a Yokogawa or Verivolt panel-mount PT with a 0–3 V AC output): wiring-only drop-in. The existing `measureChannel()` waveform-RMS algorithm works unchanged. Retain the 10 µF series AC-coupling capacitor and the 100 kΩ / 100 kΩ half-rail bias divider on A4 exactly as wired for the ZMPT101B. Calibrate `volt_scale` to the new transducer's rated output sensitivity (line V RMS ÷ ADC-pin V RMS at rated input). This is the simplest production upgrade path.
-
- **(b) 4–20 mA DC output** (e.g., NK Technologies VTR5-420-24L-DIN, UL Listed, 120 VAC input, 24 VDC loop-powered, DIN-rail mount): requires the following additions and changes not in the current BOM and firmware:
-
- - *24 VDC loop supply:* the transducer is loop-powered and requires a dedicated 24 VDC supply (e.g., MeanWell IRM-10-24 or equivalent DIN-rail 24 V module); the 5 V AC/DC supply already in the BOM cannot power it.
- - *150 Ω ±0.1% precision burden resistor:* wire from ADC A4 to GND. This converts the 4–20 mA loop current to 0.60–3.00 V DC, staying within the Cygnet's 3.3 V ADC limit. Remove the 10 µF AC-coupling capacitor and the 100 kΩ / 100 kΩ half-rail bias divider from A4; the signal is DC and needs neither AC coupling nor a bias network.
- - *Loop wiring*: connect the 24 V supply (+) → transducer VIN; transducer IOUT → burden resistor (+) → ADC A4; burden resistor (−) → GND → 24 V supply (−). Verify transducer polarity against the device datasheet.
- - *Firmware adaptation*: `measureChannel()` must be updated to replace the 2000-sample AC waveform loop with a single DC ADC read, compute instantaneous line voltage as `(adc_v - 0.60) / (3.00 - 0.60) × V_rated_full_scale` (where 0.60 V corresponds to 4 mA and 3.00 V to 20 mA), and pass that DC voltage value through the power and fault checks in place of `v_rms_adc`. The voltage-path bias and saturation checks must also be adapted — DC offset is no longer meaningful on A4, and the plausibility check should compare the derived line-voltage value directly against `VOLTAGE_MIN_V_RMS`. Implement the 4–20 mA path as a `#define`-gated alternative to the existing waveform path so both options can be compiled and bench-tested without modifying the core algorithm.
-
-**Per-phase voltage sensing** extends the design to buildings with multi-phase distribution: replace the single voltage transducer with one per phase and extend the firmware to pair each current channel with its corresponding phase voltage.
-
-**Notecard Outboard DFU** lets [firmware be updated](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) across the entire fleet without a truck roll to each panel.
-
-**Integrate with a billing platform** by configuring a Notehub HTTP route that POSTs each `meter_summary.qo` to the analytics and billing database. Configure the downstream system to reject or quarantine any `meter_summary.qo` where `fault_mask != 0` and fall back to manual estimation for the affected period. Monthly totals are derived by querying the [Notehub Event Query API](https://dev.blues.io/api-reference/notehub-api/) and summing `t*_wh` for each device over the billing period.
-
-**Capture a commissioning baseline** on first installation by recording the ADC DC offset for each channel with all loads off and storing the values in Notehub device metadata. The downstream system can compare `fault_mask` flags against these baselines to distinguish genuine zero-load readings from sensor faults.
-
-## 12. Summary
-
-The landlord who used to allocate the utility bill by square footage now allocates by actual use. Four Rogowski coils with active per-channel integrators and a voltage transducer turn the panel into a landlord-controlled, tenant-invisible metering bridge: sampling every five minutes, computing real power on-device, and delivering compact hourly summary Notes through Notehub with estimated interval energy (Wh), 15-minute blocked-average demand (W), and a per-channel fault bitmask per tenant. Monthly per-tenant totals come from querying that hourly stream in Notehub; the device never has to compute a billing figure. The readings are allocation-grade estimates accurate for proportional charge-back inside the building, not utility-tariff revenue grade — which for most landlords is exactly the right trade-off, because the alternative is either no allocation at all or a full certified sub-meter retrofit at multiples of the cost. And the deciding architecture isn't the hardware, it's the network: cellular is the only data channel that belongs entirely to the landlord and is structurally inaccessible to tenants. Any WiFi-based path puts billing data on a network the tenant can reach, and that's a dispute waiting to happen. The Notecard's landlord-owned SIM resolves the dispute by design: the panel stays dumb, the tenants stay unaware, and the allocation data arrives every hour without anyone having to think about it.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/commercial-tenant-sub-metering-bridge/)**
diff --git a/81-commercial-tenant-sub-metering-bridge/banner.png b/81-commercial-tenant-sub-metering-bridge/banner.png
deleted file mode 100644
index b368f601..00000000
Binary files a/81-commercial-tenant-sub-metering-bridge/banner.png and /dev/null differ
diff --git a/81-commercial-tenant-sub-metering-bridge/diagrams/01-system-architecture.svg b/81-commercial-tenant-sub-metering-bridge/diagrams/01-system-architecture.svg
deleted file mode 100644
index 189567da..00000000
--- a/81-commercial-tenant-sub-metering-bridge/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/81-commercial-tenant-sub-metering-bridge/diagrams/02-wiring-assembly.svg b/81-commercial-tenant-sub-metering-bridge/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 9e69535a..00000000
--- a/81-commercial-tenant-sub-metering-bridge/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,75 +0,0 @@
-
diff --git a/81-commercial-tenant-sub-metering-bridge/diagrams/03-data-flow.svg b/81-commercial-tenant-sub-metering-bridge/diagrams/03-data-flow.svg
deleted file mode 100644
index 8e5075ff..00000000
--- a/81-commercial-tenant-sub-metering-bridge/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
diff --git a/82-aerial-lift-rental-equipment-battery-health-monitor/README.md b/82-aerial-lift-rental-equipment-battery-health-monitor/README.md
index d2b976cc..86368eee 100644
--- a/82-aerial-lift-rental-equipment-battery-health-monitor/README.md
+++ b/82-aerial-lift-rental-equipment-battery-health-monitor/README.md
@@ -1,604 +1,5 @@
# Aerial Lift / Rental Equipment Battery Health Monitor
-
+A cellular and satellite reference design that turns the battery pack on a rental aerial lift into a continuously monitored asset, surfacing state of charge, depth of discharge, and thermal status.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Aerial%20Lift%20%2F%20Rental%20Equipment%20Battery%20Health%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/lift_battery_monitor/](firmware/lift_battery_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [battery management systems](https://blues.com/battery-management-systems/) reference design that turns the electric battery pack on a rental scissor lift, boom lift, or telehandler into a continuously-monitored asset, surfacing state of charge, depth of discharge, rolling state of health, and thermal status to the rental company's fleet management platform, over cellular with satellite fallback, wherever the lift happens to be sitting.
-
-## 1. Project Overview
-
-**The problem.** Electric scissor lifts, boom lifts, and telehandlers are increasingly displacing diesel units on job sites. The electric machines are quieter, cleaner, and cheaper to run, but they introduce a failure mode that diesel never had: a machine that looks fine in the yard shows up at a job site with a dead or nearly dead pack, and the rental company loses a day of revenue before anyone knows there's a problem.
-
-Rental fleets compound this. A machine might sit on a customer's site for three weeks, come back to the yard for a quick visual inspection, and go back out the next morning. Nobody is watching the pack in between. A cell group that's developed a high-impedance connection will look fine on the charger, showing full voltage at the terminals, but will drop out under load a few minutes into the shift. The customer's crew loses half a day waiting for a swap. The rental company pays for an emergency service call. The battery, which might have had years of useful life left if the fault had been caught early, gets written off.
-
-This project is the watcher. A small monitoring device clipped to the pack reads the pack bus voltage through a precision I²C power monitor (INA228) and, in field builds, pack current through either an external precision shunt wired to the INA228's differential inputs (the default, `ENABLE_ACS758 0`) or an ACS758 Hall-effect sensor on the traction conductor (the alternative, `ENABLE_ACS758 1`); records the pack temperature through a probe mounted to the battery housing; and — when the machine's **BMS** (battery management system) exposes individual cell-group voltages over **CAN** (Controller Area Network) bus — pulls those in too (see the CAN Note below). From these four data streams, it continuously estimates **SoC** (state of charge), accumulates a **cycle Ah throughput** total via a sampled Ah estimator, and maintains a rolling **SoH** (state of health) estimate updated whenever a heuristic charge-cycle threshold is crossed. Three of those numbers, plus temperature, show up on the rental company's fleet dashboard within seconds of a threshold trip, and in a single hourly telemetry Note the rest of the time.
-
-**Why Notecard.** The lift is on a construction site that is unlikely to have public WiFi, and even if it does, the rental company's fleet app has to work identically at every job site — a residential project in rural Montana and a high-rise downtown. Cellular removes that dependency entirely: no network form, no AP to pair with, no IT ticket. But construction sites can also be in coverage gaps: in-building below grade, in mountain foothills, in rural areas where the crew has gone to break ground. That's where the [Skylo](https://www.skylo.tech/) satellite fallback earns its place: the same JSON Note that normally travels over LTE-M can, transparently to the firmware, use Notecard for Skylo's NTN (Non-Terrestrial Network) radio when cellular isn't reachable. The satellite link isn't the primary path; it's the insurance policy that makes the system work the same way at every job. Notecard for Skylo integrates cellular, WiFi, and Skylo satellite on a single M.2 SoM, so there's no companion board to wire up and no secondary UART to manage.
-
-
-
-**Deployment scenario.** A sealed IP67 enclosure strapped to the battery pack housing or bolted to the machine frame near the pack, powered from the machine's 12V auxiliary circuit (derived internally from the main pack and available on most electric lifts as a utility supply). Two sensing leads run from the enclosure to the INA228 bus-voltage monitor, whose `VIN+` pad connects to the pack's positive terminal for high-side bus-voltage measurement; a single NTC probe wire runs to the pack housing; and, in field builds (`ENABLE_ACS758 1`), a separate ACS758 Hall-effect sensor connects inline on the main traction conductor for pack-current sensing. When the machine's BMS is CAN-accessible, an OBD-style cable runs to the machine's service port. No BMS modification, no charger modification, no customer involvement.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The host's job is to turn four raw signals into pack health numbers the rental company can act on. Every `sample_interval_s` seconds (default 300 seconds / 5 minutes), the Cygnet STM32 on the Notecarrier CX wakes and reads pack voltage from the INA228 over I²C, pack current from the right source for the build (INA228 `readCurrent()` through an external precision shunt in the default field build `ENABLE_ACS758 0`, the ACS758 Hall-effect sensor on A1 in the alternative `ENABLE_ACS758 1`, or the onboard INA228 shunt in bench builds `BENCH_ONLY 1`), and the pack temperature from the NTC thermistor. When the machine's BMS is CAN-accessible, the host also polls cell-group voltages over the SPI CAN interface. (**CAN BMS integration requires vendor-specific CAN ID and frame parser configuration before the feature will work with any real BMS** — the shipped firmware is a placeholder for this path; see §7.3 and §9.) From those four streams, the firmware updates SoC via a voltage-based OCV lookup, accumulates absolute current above a 0.5 A noise floor into a running cycle Ah throughput total (one sampled Ah estimate per wake), and updates a rolling SoH estimate whenever a heuristic pseudo-cycle completes (SoC dips below 30% then recovers above 90%). Threshold checks run after every sample, and any trip fires an immediate Note. Between wakes, [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) cuts host power; the Notecard idles. The full assembly still draws quiescent current from the buck regulator, INA228, thermistor divider, and any optional CAN hardware, materially above the Notecard's own idle figure, so see [§9](#9-validation-and-testing) for Mojo-measured whole-device figures.
-
-**Notecard responsibilities.** Notecard for Skylo is the part that decides when those numbers actually leave the machine. It holds [Notes](https://dev.blues.io/api-reference/glossary/#note) in its onboard queue, manages the cellular or satellite session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence, and flushes any `sync:true` alert the instant a threshold trips. It also distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from the [Blues Notehub](https://blues.com/notehub/) cloud service, so fleet-level threshold adjustments reach devices without a reflash. The built-in Skylo NTN radio activates automatically whenever terrestrial cellular is unavailable. The firmware doesn't manage the handoff, and the same alert reaches Notehub from a rural job site that it reaches from an urban yard.
-
-**Notehub responsibilities.** Once Notes leave the lift, the Notecard's embedded global SIM carries them over supported carriers worldwide and lands them in [Notehub](https://notehub.io), where every event is stored and project-level routes fan it out. Hourly telemetry summaries and immediate alerts use separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile) so the rental company can route each one to where it belongs: alerts to an on-call or rental ERP system for immediate triage, summaries to a long-term analytics store for cycle-life trending. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) are how a single firmware image serves machines with different pack chemistries and rated capacities: fleet-level environment variables encode the pack specs, and per-device overrides handle the machines that deviate.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, and Snowflake, among other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) — this project ships no specific downstream endpoint or dashboard.
-
-**Satellite data budget.** Notecard for Skylo includes 10 KB of satellite data. Hourly summaries are suppressed over satellite (the `battery_status.qo` template sets `delete:true`, so any queued summaries are cleared when the Notecard establishes an NTN session rather than being routed over the expensive satellite link). Critical alerts — low SoC, thermal excursion, SoH degradation — carry no `delete` flag and travel over whatever link is available, including satellite. This ensures fleet operators are notified of dangerous pack conditions even on job sites with no cellular coverage.
-
-## 3. Technical Summary
-
-1. **Flash firmware.** Clone the repo, edit `firmware/lift_battery_monitor/lift_battery_monitor.ino` to set your `PRODUCT_UID` (from your [notehub.io](https://notehub.io) project), then compile and upload:
- ```bash
- arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/lift_battery_monitor/
- arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/cu.usbmodem* firmware/lift_battery_monitor/
- ```
- (Replace `/dev/cu.usbmodem*` with your platform's serial port; `COM*` on Windows.)
-
-2. **Wire on the bench.** Connect INA228 over I²C (Qwiic), a 48V bench supply to INA228 `VIN+`, and an electronic load to `VIN−`. Open the serial monitor at 115200 baud and watch for `[meas]` lines reporting pack voltage and current.
-
-3. **Check Notehub.** Within one minute, the **Devices** tab should show your board. The **Events** tab will populate with `_session.qo` (confirming radio), `battery_status.qo` (hourly summary), and `battery_alert.qo` (on threshold trip). Within `report_interval_m` minutes (default 60), you should see the first summary with voltage, current, temperature, SoC, SoH, cycle Ah, and CAN health.
-
-That's it. The device is now sampling every 5 minutes, reporting hourly, and firing immediate alerts on pack fault conditions.
-
-
-Here is a sample Note this device emits:
-
-```json
-{
- "pack_v": 48.2,
- "cur_a": 12.4,
- "temp_c": 28.5,
- "soc_pct": 74,
- "soh_pct": 88,
- "throughput_ah": 42.1,
- "can_ok": true
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32 host — no separate MCU needed. Exposes I²C (Qwiic), SPI, and six ADC pins on the dual 16-pin header. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) — [Datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | 1 | Integrates LTE-M, NB-IoT, WiFi, and Skylo satellite NTN on one M.2 SoM. Cellular is the primary path; Skylo activates automatically on coverage loss. Includes 500 MB cellular + 10 KB satellite data and 10 years of service. |
-| Antenna kit (included with NOTE-NBGLWX) | 1 set | Two antennas ship with every Notecard for Skylo order: (1) a Skylo-certified LTE/S-Band/L-Band antenna for the `MAIN` u.FL port; (2) a passive GPS/GNSS antenna for the `GPS` u.FL port. Both must be mounted outdoors with an unobstructed view of the sky — route leads through dedicated cable glands in the IP67 enclosure. **Do not substitute either antenna**: the NOTE-NBGLWX is certified on Skylo's network exclusively with the supplied antennas; any replacement requires a new EIRP delta test and may cause Skylo to block the device. See the [NOTE-NBGLWX datasheet — Antenna Requirements](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) and the [antenna placement guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/). |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter on the 5V power rail for ground-truth energy validation during bring-up. Bench use only, not deployed to the field unit; see [§9](#9-validation-and-testing). |
-| [Adafruit INA228 — I²C 85V, 20-bit Power Monitor (product 5832)](https://www.adafruit.com/product/5832) | 1 | **All builds — bus-voltage sensing; default field build (`ENABLE_ACS758 0`) — also current sensing via external shunt.** Provides pack bus voltage (up to 85 V common-mode) over I²C in all configurations. In the default field build the INA228 also measures pack current through an external precision shunt wired to `VIN+`/`VIN–` (see below). In the ACS758 alternative field build (`ENABLE_ACS758 1`) the INA228 is retained for bus-voltage sensing only. In bench builds (`BENCH_ONLY 1`) it measures current through the onboard 15 mΩ shunt (≤10 A). Includes STEMMA QT connector for tool-free I²C wiring. |
-| External precision current shunt, rated for full pack discharge current at 50–75 mV full-scale *(default field build, `ENABLE_ACS758 0`, `BENCH_ONLY 0`)* | 1 | **Primary field current-sensing component.** Select a manganin or nichrome alloy shunt rated for the pack's peak discharge current (typically 50–200 A) at a full-scale voltage drop of 50–75 mV (example: 0.5 mΩ for 100 A at 50 mV; 0.375 mΩ for 200 A at 75 mV). Wire the shunt inline on the main traction conductor; connect INA228 `VIN+` to the shunt high side and `VIN–` to the shunt low side. After selecting a shunt, update `DEFAULT_SHUNT_MOHM` and `DEFAULT_SHUNT_MAX_A` in `lift_battery_monitor.ino` to match before building. Precision shunts in this spec range are stocked at Mouser and Digi-Key (search "current sense resistor manganin 200A"). |
-| [Allegro ACS758LCB-200B-PFF-T](https://www.allegromicro.com/~/media/Files/Datasheets/ACS758-Datasheet.ashx) Hall-effect current sensor *(alternative field build, set `ENABLE_ACS758 1`)* | 0–1 | **Optional alternative to the external shunt for field current sensing.** Bidirectional ±200 A **inline** Hall-effect sensor. Use this path if breaking and re-terminating the traction conductor for an inline shunt is impractical, or if galvanic isolation from the traction bus is desired. Installation requires the main traction conductor to be broken and re-terminated through the sensor's primary conductor terminals (`IP+` → `IP–`); this is not a clamp-on or non-invasive part. The conductor work must be performed by qualified personnel following machine-specific lockout/tagout procedures. **Requires 5 V supply** (power from the Pololu 5 V rail, not Notecarrier CX +3V3; the ACS758 VCC minimum is 4.5 V). At VCC = 5 V the output is ratiometric: 2.5 V at 0 A, ±10 mV/A sensitivity. At +200 A the output reaches 4.5 V, which exceeds the Cygnet's 3.3 V ADC rail — a 10 kΩ / 20 kΩ voltage divider on `VOUT` is required (see §5). Set `ENABLE_ACS758` to `1` in `lift_battery_monitor_config.h` before building. Stocked at Digi-Key and Mouser by full part number **ACS758LCB-200B-PFF-T**. |
-| 10 kΩ resistor, 1% or 5%, 1/4 W *(ACS758 alternative build only, `ENABLE_ACS758 1`)* | 0–1 | High-side leg of the ACS758 `VOUT` voltage divider. Together with the 20 kΩ low-side resistor, scales the 0–4.5 V ACS758 output to the Cygnet's 3.3 V ADC rail (×2/3 ratio). See §4 ACS758 wiring. Not required for the external-shunt default path. |
-| 20 kΩ resistor, 1% or 5%, 1/4 W *(ACS758 alternative build only, `ENABLE_ACS758 1`)* | 0–1 | Low-side leg of the ACS758 `VOUT` voltage divider (from the `A1`/divider mid-point to GND). Not required for the external-shunt default path. |
-| 100 nF ceramic capacitor, 10 V or higher *(ACS758 alternative build only, `ENABLE_ACS758 1`)* | 0–1 | Local bypass capacitor on the ACS758 `VCC` supply pin, placed as close to the sensor pins as possible. Suppresses high-frequency noise on the 5 V rail that would otherwise couple into the ratiometric `VOUT` output and add current-measurement error. Not required for the external-shunt default path. |
-| 10 kΩ NTC thermistor, β=3950, waterproof probe | 1 | Pack housing temperature. Waterproof probe survives the enclosure penetration and battery-bay humidity. |
-| 10 kΩ 1% resistor (thermistor divider) | 1 | Pull-up for the thermistor voltage divider. 1% tolerance keeps temperature error below ±1°C across the range of interest. |
-| Waveshare MCP2515 CAN Bus Module (MCP2515 controller + TJA1050 transceiver, ~23 × 33 mm, 5V logic) *(optional)* | 0–1 | Reads cell-group voltages from the machine's BMS CAN bus. This specific module provides the compact footprint needed for a sealed enclosure; do not substitute an Arduino-shield-form CAN board. Requires the level shifter described in [§5](#5-wiring-and-assembly). **The machine-side CAN connector and cable are machine-specific** — the OBD/service-port connector type, pinout, and cable gauge depend on the machine manufacturer and model and are not part of this BOM. Consult the machine's service manual for the CAN bus access port details before ordering a mating connector or cable. |
-| [Adafruit TXB0104 Quad Bi-Directional Level Shifter (product 1875)](https://www.adafruit.com/product/1875) *(optional, required with CAN module)* | 0–1 | Translates the 3.3V Cygnet SPI signals (MOSI, MISO, SCK, CS) to the 5V logic level required by the Waveshare MCP2515 module. The TXB0104 uses a push-pull output stage rated for SPI — **do not substitute a MOSFET-based open-drain level converter** (such as the SparkFun BOB-12009), which is designed for I²C and is unreliable on push-pull SPI lines. |
-| [Pololu 5V, 2.5A Step-Down Voltage Regulator D24V22F5](https://www.pololu.com/product/2858) | 1 | Powers the Notecarrier CX (VBAT+ accepts 2.5–5.5V) and the optional CAN module from the machine's 12V auxiliary circuit. Accepts up to 36V in; 2.5A continuous output is ample headroom. |
-| IP67 waterproof enclosure, ~6×4×2″ | 1 | Protects electronics from battery-bay splash, dust, and occasional hosing during machine wash-down. |
-
-All Blues hardware ships with an active SIM and no activation fees or monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. Notecard for Skylo seats in the carrier's M.2 slot. The Mojo sits inline between the 5V supply output and the Notecarrier's VBAT+ pad during bench validation.
-
-
-
-**⚠ Safety.** Battery packs on electric aerial lifts carry potentially lethal voltages (24–80V DC) and very high fault currents. All connections to the high-voltage pack circuit must be made by qualified personnel following applicable electrical codes and the machine manufacturer's service procedures. Use appropriately rated wire, fusing, and insulation for all connections to the pack. The INA228 and its associated wiring must be rated for the pack's full voltage and anticipated fault current. This project does not provide a connection to the machine's control system and cannot command motion or charging.
-
-
-
-**Pack-bus-voltage sensing — I²C (INA228) — all builds:**
-
-I²C connections (same for all builds):
-- Notecarrier CX **SDA** → INA228 STEMMA QT `SDA` (or breakout `SDA` pin)
-- Notecarrier CX **SCL** → INA228 STEMMA QT `SCL` (or breakout `SCL` pin)
-- Notecarrier CX **+3V3** → INA228 `VCC`
-- Notecarrier CX **GND** → INA228 `GND`
-
-All INA228 logic signals operate at 3.3 V referenced to pack-negative ground, which is also the Cygnet's GND rail.
-
-**Pack-current sensing — primary field wiring (external precision shunt, `ENABLE_ACS758 0`, `BENCH_ONLY 0`):**
-
-The external shunt is wired **inline on the main traction conductor** — current flows through it just as with the INA228 bench shunt or the ACS758, but a properly rated external shunt carries the full traction current safely. Wire the INA228 `VIN+` and `VIN–` differentially across the shunt.
-
-
-
-**⚠ High-current conductor work.** Breaking and re-terminating the traction conductor for an inline shunt must be performed by qualified personnel with the pack fully isolated — main contactor open, pack-negative bus bar disconnected or insulated. Follow the machine manufacturer's lockout/tagout procedures before handling the traction conductor.
-
-
-
-High-voltage connections (external shunt primary field path):
-- Battery pack positive terminal → external shunt `IN+` (high side) → INA228 `VIN+`
-- External shunt `IN–` (low side) → main load/charger positive bus and INA228 `VIN–`
-- Battery pack negative terminal → system GND → INA228 `GND`
-
-`readCurrent()` returns calibrated amps because `setShunt()` is called at startup with `DEFAULT_SHUNT_MOHM * 0.001f` and `DEFAULT_SHUNT_MAX_A`. **Update those two constants in `lift_battery_monitor.ino` to match the installed shunt before building.** Insulate all wires for the pack's full voltage rating.
-
-**Pack-current sensing — alternative field wiring (ACS758LCB-200B-PFF-T, `ENABLE_ACS758 1`):**
-
-The ACS758LCB-200B-PFF-T is an **inline** Hall-effect current sensor, not a clamp-on or non-invasive part. The pack's main traction conductor must be **broken and re-terminated** through the sensor's primary conductor terminals (`IP+` → `IP–`). Use this path if galvanic isolation from the traction bus is required, or if the external shunt path is impractical.
-
-
-
-**⚠ High-current conductor work.** Breaking and re-terminating the traction conductor must be performed by qualified personnel with the pack fully isolated — main contactor open, pack-negative bus bar disconnected or insulated. Follow the machine manufacturer's lockout/tagout procedures before handling the traction conductor.
-
-
-
-- Battery pack positive conductor → ACS758 `IP+` terminal
-- ACS758 `IP–` terminal → main load/charger positive bus
-- **5 V supply rail** (Pololu D24V22F5 output) → ACS758 `VCC` (place a 100 nF ceramic capacitor between `VCC` and `GND` close to the sensor pins). **Do not use Notecarrier CX +3V3** — the ACS758 VCC minimum is 4.5 V; it will not operate correctly from 3.3 V.
-- Notecarrier CX **GND** → ACS758 `GND`
-- ACS758 `VOUT` → 10 kΩ resistor → Notecarrier CX `A1`; 20 kΩ resistor from the `A1` node to **GND**
-
-At VCC = 5 V the sensor is ratiometric: VOUT = 2.5 V at 0 A, ±10 mV/A sensitivity. At full-scale +200 A discharge, VOUT = 4.5 V — which exceeds the Cygnet's 3.3 V ADC rail. The 10 kΩ / 20 kΩ voltage divider scales VOUT by 2/3, so VADC_max = 3.0 V. Conversion back to current:
-
-```
-curA = (VADC × 1.5 − 2.5) / 0.010
-```
-
-One ADC count (3.3 V / 4095) maps to approximately 0.121 A. Set `ENABLE_ACS758` to `1` in `lift_battery_monitor_config.h` before building — `readPackVI()` performs this conversion automatically:
-
-```cpp
-// Alternative field path — ENABLE_ACS758 1
-// VCC = 5V; VIOUT = 2.5V + (I × 10 mV/A); 10kΩ/20kΩ divider scales × 2/3
-float vadc_v = analogRead(A1) * (3.3f / 4095.0f);
-curA = (vadc_v * 1.5f - 2.5f) / 0.010f;
-```
-
-In this path the INA228 `readCurrent()` call is not used. Wire **both** INA228 `VIN+` and `VIN–` to the pack positive terminal so no traction current flows through the onboard shunt:
-
-- Battery pack positive terminal → INA228 `VIN+` **and** `VIN−` (connect **both** pads to the same pack-positive node)
-- Battery pack negative terminal → system GND → INA228 `GND`
-
-
-
-**Why both VIN+ and VIN− go to pack positive in the ACS758 path.** The Adafruit INA228 breakout has a 15 mΩ shunt hard-wired between the `VIN+` and `VIN−` pads on the PCB. If `VIN−` were wired to the load/charger positive bus, the full pack discharge current would flow through that onboard shunt — even if firmware never calls `readCurrent()`. At 50–200 A traction currents the shunt would be destroyed and could become a hazard. Shorting both pads to pack positive puts zero volts across the shunt; `readBusVoltage()` still returns the full pack voltage at `VIN+` relative to GND, unaffected by this wiring. Insulate both `VIN+` and `VIN−` wires for the pack's full voltage rating.
-
-
-
-
-
-**Pack-bus-voltage and current sensing — bench/prototype wiring (INA228 onboard shunt, `BENCH_ONLY 1`, ≤10 A only):**
-
-⚠ **Bench/prototype only.** The INA228's onboard 15 mΩ shunt is rated to approximately 10 A continuous. **Do not place the onboard shunt inline on a traction-pack conductor at real lift discharge currents (50–200 A).** This path is for bench testing with a bench supply and current-limited load only. For any field installation use the external shunt primary wiring above.
-
-
-
-In bench builds where pack current stays below 10 A, the INA228 can simultaneously sense bus voltage and current from a single inline placement. The I²C connections are the same as documented above; add the high-voltage circuit connections below (Note that `VIN−` wiring differs from the ACS758 path, here it goes to the bench load, not back to pack positive):
-- Battery pack positive terminal → INA228 `VIN+` (shunt high side)
-- INA228 `VIN–` (shunt low side) → bench load positive terminal
-- Battery pack negative terminal → system GND → INA228 `GND`
-
-`readCurrent()` is valid in this configuration because the 15 mΩ shunt bridges `VIN+` and `VIN–`. `setShunt()` is called with the onboard shunt values (`0.015 Ω`, `10 A`) when `BENCH_ONLY 1`; no register change is needed for bench testing.
-
-> **Note on the Notecarrier CX errata.** The v1.3 board silkscreen has the `MOSI` and `MISO` labels swapped on the dual 16-pin header. When wiring the CAN module's SPI, verify pin function in the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) rather than trusting the header labels.
-
-**Thermistor (A0):**
-- Notecarrier CX **+3V3** → 10 kΩ 1% series resistor → Notecarrier CX **A0**
-- Notecarrier CX **A0** (same junction) → NTC probe top lead → Notecarrier CX **GND**
-- Mount the NTC probe against the pack housing with thermal compound and secure with a cable tie or adhesive pad.
-
-**CAN module (optional, SPI, via level shifter):**
-
-The Waveshare MCP2515 CAN Bus Module runs at 5V logic; all SPI lines must pass through the **Adafruit TXB0104** level shifter first. The TXB0104 uses a push-pull output stage and is rated for SPI — do not substitute a MOSFET-based open-drain converter. Power the CAN module from the 5V supply rail (the same output as the Pololu D24V22F5), not from the Notecarrier's +3V3 header pin.
-
-- 5V supply (Pololu D24V22F5 output, or Mojo `LOAD` during bench validation. See **Power chain** below) → CAN module `VCC` and TXB0104 `VCCB`
-- Notecarrier CX **+3V3** → TXB0104 `VCCA`; also connect TXB0104 `OE` to `VCCA` to enable the translator
-- Notecarrier CX **GND** → TXB0104 `GND` and CAN module `GND`
-- Notecarrier CX header **MOSI** → TXB0104 `A1` ↔ TXB0104 `B1` → CAN module `SI`
-- Notecarrier CX header **MISO** → TXB0104 `A2` ↔ TXB0104 `B2` ← CAN module `SO`
-- Notecarrier CX header **SCK** → TXB0104 `A3` ↔ TXB0104 `B3` → CAN module `SCK`
-- Notecarrier CX header **D5** → TXB0104 `A4` ↔ TXB0104 `B4` → CAN module `CS`
-- CAN module `CANH` / `CANL` → machine OBD/CAN service port `CAN_H` / `CAN_L`
-- **Do not add a terminator at the device end** when connecting to an existing machine service-port stub. The vehicle/machine CAN backbone is already terminated at both ends; a third resistor loads the bus and can disrupt BMS communications. Only add a 120 Ω resistor if you have verified with a DMM that the segment is unterminated and the monitor is at a true electrical end of a dedicated CAN run.
-
-**Ground reference — verify before connecting:**
-
-> **⚠ Commissioning check.** This design bonds the machine's 12 V auxiliary negative, the pack negative, and the monitor's logic GND to a common rail. That assumption holds on machines where the 12 V auxiliary supply is derived directly from the main traction pack (the majority of electric aerial lifts), but some machines use an isolated 12 V DC/DC converter that is not referenced to pack negative. **Before making any connections:** measure DC voltage between the 12 V auxiliary negative terminal and the pack negative terminal with a multimeter. A reading near 0 V confirms a common reference and the wiring below is safe. Any other reading indicates an isolated auxiliary supply — in that case, use an isolated 5 V DC/DC converter module (e.g., a Murata or Vicor isolated SIP brick) to power the monitoring circuit, and Note that connecting INA228 `GND` to the monitor's logic GND would bridge the isolation barrier; isolated sensing must be evaluated and approved by a qualified electrical engineer before proceeding.
-
-**Power chain:**
-
-> **⚠ Commissioning check — always-on aux rail.** The monitor must remain powered whenever the main traction pack is energized, including when the machine is keyed off, the charger is disconnected, and the machine is parked in storage. Dead-on-arrival conditions (parasitic discharge, thermal excursions during storage, charger faults at night) develop precisely during these intervals. **Before wiring the 12 V aux feed, verify with a multimeter that it stays live in all of the following states: key off, charger unplugged, master disconnect closed.** If the rail drops in any of those states, do not use it as the monitor's power source. Instead, run the input of the Pololu DC/DC converter directly from the main traction-pack positive terminal through an appropriately rated fuse (≤3 A slow-blow, wire gauge rated for the full pack voltage). The Pololu D24V22F5 accepts up to 36 V; for packs above 36 V substitute a DC/DC converter rated for the full pack voltage (up to 85 V to match the INA228's common-mode limit). See §9 for the associated deployment limitation.
-
-*Field deployment (Mojo not installed):*
-- Machine 12V aux (confirmed always-on. See commissioning check above) → DC/DC converter input → 5V rail → Notecarrier CX **VBAT+** and CAN module `VCC` / TXB0104 `VCCB` (if installed)
-- Machine chassis **GND** → DC/DC converter GND → Notecarrier CX **GND**
-
-*Bench validation (Mojo installed):*
-- Machine 12V aux → DC/DC converter input → 5V rail → Mojo `BAT` → Mojo `LOAD` → Notecarrier CX **VBAT+** and CAN module `VCC` / TXB0104 `VCCB` (if installed)
-- Route **all** 5V loads from the Mojo `LOAD` pin so the trace captures complete 5V subsystem current, including the CAN module and level shifter, not the Notecard/Notecarrier branch alone.
-- Machine chassis **GND** → DC/DC converter GND → Notecarrier CX **GND**
-
-**Antenna (Notecard for Skylo):**
-- Use the two antennas included in the NOTE-NBGLWX kit (see §4 BOM). Connect the Skylo-certified LTE/S-Band/L-Band antenna to the `MAIN` u.FL port and the passive GPS/GNSS antenna to the `GPS` u.FL port.
-- Route both antenna leads through dedicated cable glands to the exterior of the IP67 enclosure. Both antennas must be placed outdoors with a clear view of the sky; no metallic obstruction between the antenna and the sky for the `MAIN` antenna is required for Skylo satellite acquisition.
-- Do not substitute either antenna. See the §3 BOM row for the certification implications. See the [antenna placement guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/) for routing and clearance best practices.
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) (it looks like `com.your-company.your-name:lift-battery`) and paste it into the `#define PRODUCT_UID ""` line in the firmware sketch.
-
-2. **Claim the Notecard.** Power the assembled unit. The Notecard associates with your project on first cellular session — no manual claim step. The device appears in the **Devices** tab within a few minutes.
-
-3. **Create a Fleet per machine category.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. A natural scheme for a rental company: one fleet per pack chemistry and voltage (e.g., "Fleet-Lithium-48V", "Fleet-LeadAcid-48V"). Assign new devices to the correct fleet manually on first claim, or use [per-device environment overrides](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) for machines that deviate from the fleet spec — the firmware does not emit a `pack_chemistry` field in telemetry, so Smart Fleet rules cannot match on chemistry automatically.
-
-4. **Set environment variables.** In Notehub: **Fleets → your fleet → Environment** (or **Devices → your device → Environment** for a per-machine override). All variables are optional; firmware defaults are shown. The device picks up changes on its next inbound sync — no reflash, no truck roll.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `soc_alert_pct` | `20.0` | SoC (%) below which `soc_low` alert fires. |
- | `temp_high_c` | `45.0` | Pack temperature (°C) above which `temp_high` fires. |
- | `temp_low_c` | `5.0` | Pack temperature (°C) below which `temp_low` fires (charging damage risk). |
- | `soh_alert_pct` | `70.0` | Rolling SoH (%) below which `soh_low` alert fires. |
- | `rated_cap_ah` | `100.0` | Nameplate capacity of the pack in Ah. Used as the denominator in SoH estimation — set this correctly for your machine model. |
- | `chemistry` | `lithium` | Battery chemistry for the OCV→SoC lookup table. Accepts `lithium` (LiFePO4-biased) or `lead_acid`. |
- | `cell_delta_mv` | `200.0` | Maximum permitted cell-group voltage imbalance in mV (CAN only). Above this threshold, `cell_imbalance` fires. |
- | `sample_interval_s` | `300` | Seconds between samples. The host sleeps between wakes via `card.attn`. |
- | `report_interval_m` | `60` | Minutes between summary Notes. On the next wake after a Notehub change, the firmware re-applies `hub.set` so the Notecard outbound cadence stays aligned with the new value — no reboot required. |
- | `acs758_zero_v` | `2.5` | *(Field builds, `ENABLE_ACS758 1` only.)* ACS758 output voltage at zero current (V). Nominal 2.5 V at VCC = 5 V; measure with the machine powered and the main contactor open (no traction current) at commissioning and set to the observed value to calibrate out Hall-sensor zero-offset error. A biased zero point shifts every Ah accumulation sample, corrupting throughput and SoH over time. |
- | `acs758_mv_per_a` | `10.0` | *(Field builds, `ENABLE_ACS758 1` only.)* ACS758 sensitivity (mV/A). 10.0 mV/A for the ACS758LCB-200B variant specified in the BOM; consult the datasheet if a different variant is substituted. |
-
-5. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) targeting `battery_alert.qo` (to an on-call endpoint or rental ERP webhook for real-time triage) and a second targeting `battery_status.qo` (to a long-term analytics store for cycle-life trending). Separating the two Notefiles at the source means you can deliver them to different systems at different urgency levels without any filtering inside the route.
-
-### What you should see in Notehub
-
-Within a minute of first power-on, the **Events** tab should start populating. Three event types appear:
-
-- **`_session.qo`** (appears first) — automatic Notecard housekeeping on each cellular or satellite session. Confirms the radio is reaching Notehub. No custom body; the Notecard logs this automatically.
-- **`battery_status.qo`** (appears after `report_interval_m` minutes; default 60) — hourly rolling summary with all data from the sample window. Sample body:
- ```json
- {
- "pack_v": 48.2,
- "cur_a": 12.4,
- "temp_c": 28.5,
- "soc_pct": 74,
- "soh_pct": 88,
- "throughput_ah": 42.1,
- "can_ok": true
- }
- ```
- **Field Notes:**
- - `cur_a` is positive during discharge. Negative during charge (if the pack is being actively charged).
- - All seven fields are always present. Expect them on every summary.
- - `temp_c` is averaged over valid (non-NaN) reads only. If the thermistor produces no valid reads in the window, `temp_c` will be `-9999.0` — check thermistor wiring if you see this alongside reasonable `pack_v`.
- - `can_ok` is always `false` when `ENABLE_CAN_BMS` is 0 (the default); this is expected behavior, not a fault. It only reflects live CAN bus health when `ENABLE_CAN_BMS` is 1 and a CAN module is wired.
-
-- **`battery_alert.qo`** (appears only on threshold trip) — emitted immediately (within 15–60 seconds) with `sync:true` for all alert types except `can_error`. All five fields always present: `alert` (string), `pack_v` (V), `soc_pct` (%), `temp_c` (°C), `extra_v` (alert-specific value). See §7 for the per-alert payload shapes. `can_error` is the explicit exception: it queues for normal outbound delivery (not `sync:true`) and is suppressed once per hour after each emission to avoid flooding the Notefile if the BMS is powered off.
-
-## 7. Firmware Design
-
-Four files in [`firmware/lift_battery_monitor/`](firmware/lift_battery_monitor/):
-
-- [`lift_battery_monitor_config.h`](firmware/lift_battery_monitor/lift_battery_monitor_config.h) — **build-configuration toggles** (`ENABLE_CAN_BMS`, `ENABLE_ACS758`, `BENCH_ONLY`). **This is the one file to edit** when changing build options; it is included by both the `.ino` and the `.cpp` so both translation units always agree.
-- [`lift_battery_monitor.ino`](firmware/lift_battery_monitor/lift_battery_monitor.ino) — global declarations, `setup()`, and `loop()`
-- [`lift_battery_monitor_helpers.cpp`](firmware/lift_battery_monitor/lift_battery_monitor_helpers.cpp) — all function implementations: Notecard config, sensor reads, SoC/SoH estimation, alert logic
-- [`lift_battery_monitor_helpers.h`](firmware/lift_battery_monitor/lift_battery_monitor_helpers.h) — shared types (`PersistState`, `Config`), `extern` declarations, and function prototypes
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Install via Arduino IDE Boards Manager (search "STM32 MCU based boards") or add the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**. Select **Blues Cygnet** as the board target (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-- **`Blues Wireless Notecard`**: install via Arduino Library Manager or `arduino-cli lib install "Blues Wireless Notecard"`. See [note-arduino releases](https://github.com/blues/note-arduino/releases) for the latest.
-- **`Adafruit INA228`**: install via Library Manager or `arduino-cli lib install "Adafruit INA228"`. Pulls in `Adafruit BusIO` as a dependency. The firmware calls `readBusVoltage()` and `readCurrent()`, the modern API defined on the `Adafruit_INA2xx` base class; these names were verified against the current library source (`Adafruit_INA2xx.h`).
-- **`mcp2515`** by autowp *(only if `ENABLE_CAN_BMS` is set to `1` in `lift_battery_monitor_config.h`)* — install via Library Manager or `arduino-cli lib install "mcp2515"`.
-
-**Flashing — Arduino IDE:** open `lift_battery_monitor.ino`, select Cygnet as the board, click **Upload**. The Notecarrier CX's built-in ST-Link means no external programmer is needed.
-
-**Flashing — `arduino-cli`:**
-
-First, confirm your STM32 core version and FQBN:
-```bash
-arduino-cli board listall | grep -i cygnet
-```
-
-The canonical FQBN is `STMicroelectronics:stm32:Blues:pnum=CYGNET`; substitute whatever `listall` reports if your installed core differs. Use this compile and upload sequence:
-```bash
-# Compile
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/lift_battery_monitor/
-
-# Upload (replace /dev/cu.usbmodem* with your serial port)
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/cu.usbmodem* firmware/lift_battery_monitor/
-```
-
-**Platform-specific serial ports:**
-- **macOS:** `/dev/cu.usbmodem*` (wildcard matches the Notecarrier CX's ST-Link VCP)
-- **Linux:** `/dev/ttyACM*` or `/dev/ttyUSB*` (check `ls /dev/tty*` after plugging in)
-- **Windows:** `COM3` (or higher; check Device Manager → Ports)
-
-Open the serial monitor at **115200 baud** to watch `[meas]` lines during bring-up. After each sample cycle, the host sleeps for `sample_interval_s` seconds — the serial output will go quiet until the next wake. That's normal; it means `card.attn` is correctly cutting host power.
-
-### 7.2 Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard configuration (`hub.set`, templates) | `notecardConfigure`, `defineTemplates` |
-| Env-variable fetch and clamp | `fetchEnvOverrides` |
-| Pack voltage (INA228) + current (external shunt via INA228, or ACS758 on A1) | `readPackVI` |
-| NTC thermistor read | `readPackTempC` |
-| OCV→SoC lookup + linear interpolation | `voltageToSoC` |
-| Sampled Ah throughput accumulator and SoH update | `updateThroughput`, `updateSoH` |
-| Alert evaluation and emission | `checkAlerts`, `sendAlert` |
-| Hourly summary emission | `sendSummary` |
-| Optional CAN BMS cell-group read | `pollCanBms` *(#if ENABLE_CAN_BMS)* |
-| Persistent state across sleep cycles | `PersistState` + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### 7.3 Sensor reading strategy
-
-**INA228.** The Adafruit INA228 (0x40 default I²C address) provides a 20-bit measurement of bus voltage (up to 85 V) and, when configured for it, differential current through an external shunt wired to `VIN+`/`VIN–`. The firmware always calls `readBusVoltage()` to read pack bus voltage. In the default field build (`ENABLE_ACS758 0`, `BENCH_ONLY 0`) it also calls `readCurrent()` for current through the external field shunt; in the ACS758 alternative build (`ENABLE_ACS758 1`) `readCurrent()` is not called and pack current comes from the ACS758 Hall-effect sensor on A1 instead; in bench builds (`BENCH_ONLY 1`) `readCurrent()` returns current through the onboard 15 mΩ shunt. A single I²C transaction for bus voltage takes under 5 ms. `setShunt()` is called at startup with `DEFAULT_SHUNT_MOHM * 0.001f` and `DEFAULT_SHUNT_MAX_A` (field/external shunt path) or `0.015 Ω` / `10 A` (bench path), writing the SHUNT_CAL register to calibrate `readCurrent()` for the installed shunt.
-
-**Thermistor.** Sixteen-sample average of 12-bit ADC counts on A0, converted to resistance via the divider ratio, then converted to temperature via the β (Steinhart-Hart simplified) equation: `T = 1 / (1/T₀ + (1/β) × ln(R/R₀))`.
-
-**CAN BMS** *(conditional).* When `ENABLE_CAN_BMS` is 1, the MCP2515 is initialized at 250 kbps (configurable), and each wake polls for pending CAN frames up to a short timeout. Frames matching `BMS_CELL_GROUP_ID` are parsed as 2-byte big-endian cell-group voltages in millivolts. A classic CAN frame carries at most 8 data bytes (DLC ≤ 8), so at most four 16-bit values fit in one frame; the placeholder parser extracts however many complete pairs the frame's DLC permits. The exact CAN ID, frame layout, and whether multi-frame messages are used are all BMS-specific. The firmware ships a placeholder; the developer must update `BMS_CELL_GROUP_ID` and `parseCellGroupFrame()` to match the actual BMS protocol. Common rental-equipment BMS vendors expose cell voltages on SAE J1939 parameter groups at 250 kbps; consult the machine's service documentation.
-
-### 7.4 Event payload design
-
-Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles. Compact format (`format: "compact"`) with unique port numbers is required so Notes are compatible with satellite transmission. `battery_status.qo` carries `delete: true`, which instructs the Notecard to discard any queued summaries if the Notecard establishes a satellite NTN session (rather than sending several hours of accumulated summaries over the metered satellite link). `battery_alert.qo` has no `delete` flag — critical alerts must get through on any available transport.
-
-**`battery_status.qo` hourly summary:**
-
-```json
-{
- "pack_v": 48.2,
- "cur_a": 12.4,
- "temp_c": 28.5,
- "soc_pct": 74,
- "soh_pct": 88,
- "throughput_ah": 42.1,
- "can_ok": true
-}
-```
-
-**`battery_alert.qo` immediate alert (example, `soc_low`):**
-
-```json
-{
- "alert": "soc_low",
- "pack_v": 43.1,
- "soc_pct": 18,
- "temp_c": 27.3,
- "extra_v": 0.0
-}
-```
-
-All five fields are always present on every alert Note. `extra_v` is `0.0` for alert types that carry no auxiliary value (`soc_low`, `can_error`); for other types it carries alert-specific context (see §8).
-
-### 7.5 Low-power strategy
-
-The sidecar draws power from the machine. Even so, keeping the host genuinely asleep minimizes heat dissipation in the battery bay and ensures the monitoring device itself doesn't materially drain the pack it's monitoring. After each sample cycle, the host calls `NotePayloadSaveAndSleep`, which serializes the `PersistState` struct into Notecard flash and issues a [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) `sleep` request to cut host power for `sample_interval_s` seconds. On the next wake the MCU re-enters `setup()` from cold, and `NotePayloadRetrieveAfterSleep` rehydrates the state: SoC, SoH accumulators, alert cooldowns, and the running summary window all survive the sleep cycle.
-
-The Notecard itself idles at ~8–18 µA between cellular sessions; this is the Notecard SoM's own draw from the datasheet. The assembled device additionally draws quiescent current from the buck regulator, INA228 breakout (always-on I²C), thermistor divider, and any optional CAN hardware, so the whole-device idle figure at the 5V rail is materially higher. Use Mojo during bench validation to measure the actual assembled current rather than relying on the Notecard datasheet figure alone; see [§9](#9-validation-and-testing). Sampling runs at 5-minute intervals; transmission runs at 60-minute intervals. When `report_interval_m` is changed via Notehub, the firmware re-applies `hub.set` on the next wake so the Notecard outbound cadence stays in sync without requiring a reboot. All alerts except `can_error` bypass the transmit timer entirely via `sync: true`. `can_error` is the explicit exception: it fires on the first failed CAN poll (once the Notecard clock is set), then is suppressed for one hour after each emission; it queues for normal outbound delivery rather than triggering an immediate cellular session.
-
-### 7.6 Retry and error handling
-
-- The first Notecard transaction uses `sendRequestWithRetry(req, 10)` to paper over the cold-boot I²C race documented in the note-arduino library.
-- If `readPackVI()` returns values outside the plausible range (bus voltage < 10 V or > 85 V, the INA228's common-mode hardware limit, or current outside ±200 A), or if the INA228 is not found at startup, the firmware aborts that entire sample cycle: it persists state and goes to sleep immediately, without advancing the summary accumulators, updating SoC/SoH/DoD, or evaluating any alert thresholds. `readPackTempC()` is called before `readPackVI()` so temperature has already been read, but its value is discarded when the cycle aborts — the thermistor accumulator does not advance for that wake. Alert cooldown epochs survive the abort in persisted state. If all wakes in a `report_interval_m` window abort, `summ_count` will be zero. When the report timer fires on an empty window, `sendSummary()` advances `last_summ_epoch` without emitting a Note, starting a fresh full report interval from that point. This ensures the first successful sample after a prolonged sensor fault begins a new window rather than immediately triggering an under-populated summary. When a summary is sent, all seven fields are always present; `temp_c` uses only valid (non-NaN) thermistor reads in its average and will be `-9999.0` if the thermistor produced no valid reads across the successful samples in the window.
-- Alert cooldowns (`ALERT_COOLDOWN_SEC`, default 30 minutes) prevent a slow-drifting threshold from paging the on-call every sample cycle.
-- CAN read failures (when enabled) set `can_ok: false` in the next summary. A `can_error` alert fires immediately on the first failed poll (once the Notecard clock is set); subsequent failures emit at most once per hour. CAN failures do not block the non-CAN sensors from reporting normally.
-
-### 7.7 Key code snippet 1: satellite-aware template definition
-
-`format: "compact"` is required for Notes to be transmittable over the Skylo NTN satellite link. The `port` is a unique integer (1–100) that lets the Notecard reference the Notefile over the air by number rather than full string. `delete: true` on the hourly summary prevents it from being routed over the metered satellite link.
-
-**Compact format type hints:** The template body uses numeric "hints" to encode field types for on-wire compression:
-- `14.1` = 32-bit IEEE 754 float (4 bytes) — used for voltages, currents, temperatures, Ah
-- `21` = 8-bit unsigned integer (1 byte) — used for percentages (SoC, SoH, 0–100)
-- `true` / `false` = boolean (1 bit, packed) — used for status flags like `can_ok`
-
-These type codes are how the Notecard knows to pack `pack_v: 48.2` and `soc_pct: 74` into minimal bytes for satellite transmission.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "battery_status.qo");
-JAddNumberToObject(req, "port", STATUS_PORT); // 50
-JAddStringToObject(req, "format", "compact");
-JAddBoolToObject(req, "delete", true); // discard queued summaries if satellite connects
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "pack_v", 14.1); // note-arduino compact type hint: 4-byte float32
-JAddNumberToObject(body, "cur_a", 14.1);
-JAddNumberToObject(body, "temp_c", 14.1);
-JAddNumberToObject(body, "soc_pct", 21); // 1-byte uint8
-JAddNumberToObject(body, "soh_pct", 21);
-JAddNumberToObject(body, "throughput_ah", 14.1);
-JAddBoolToObject(body, "can_ok", true);
-notecard.sendRequest(req);
-```
-
-### 7.8 Key code snippet 2: immediate-sync alert
-
-`sync: true` wakes the Notecard's radio immediately rather than waiting for the next hourly outbound window. If cellular isn't available, Notecard for Skylo will establish a Skylo satellite session instead.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "battery_alert.qo");
-// port is bound in note.template (ALERT_PORT = 51); it is not a valid note.add field.
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "soc_low");
-JAddNumberToObject(body, "pack_v", packV);
-JAddNumberToObject(body, "soc_pct", (int)socPct);
-JAddNumberToObject(body, "temp_c", isnan(tempC) ? -9999.0f : tempC);
-JAddNumberToObject(body, "extra_v", 0.0f); // always present; 0.0 when no aux value
-notecard.sendRequestWithRetry(req, 10);
-```
-
-### 7.9 Key code snippet 3: sleep with state persistence
-
-The persistent state struct is serialized to Notecard flash before the host powers down. On the next wake, `NotePayloadRetrieveAfterSleep` rehydrates it: accumulator totals, alert cooldowns, and SoH history all survive.
-
-```cpp
-NotePayloadDesc payload = {0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, cfg.sample_interval_s, NULL);
-// Execution does not return here; the Notecard cuts host power.
-```
-
-### 7.10 Key code snippet 4: SoC interpolation from OCV lookup table
-
-Linear interpolation between table entries smooths the SoC curve. The table is selected at runtime based on the `chemistry` environment variable.
-
-```cpp
-float voltageToSoC(float voltage, bool isLithium) {
- const float (*tbl)[2] = isLithium ? SOC_TABLE_LITHIUM : SOC_TABLE_LEAD_ACID;
- const int n = isLithium ? SOC_LITHIUM_ROWS : SOC_LEAD_ACID_ROWS;
- if (voltage >= tbl[0][0]) return tbl[0][1];
- if (voltage <= tbl[n-1][0]) return tbl[n-1][1];
- for (int i = 0; i < n - 1; i++) {
- if (voltage >= tbl[i+1][0]) {
- float slope = (tbl[i][1] - tbl[i+1][1]) / (tbl[i][0] - tbl[i+1][0]);
- return tbl[i+1][1] + slope * (voltage - tbl[i+1][0]);
- }
- }
- return 0.0f;
-}
-```
-
-## 8. Data Flow
-
-
-
-Every `sample_interval_s` seconds the firmware wakes, reads up to four data sources, and decides what to emit. The two transmit paths — alerts and summaries — run on independent timers and are completely decoupled from each other.
-
-**Collected** (per wake):
-- Pack bus voltage (V) — INA228 over I²C (`readBusVoltage()`), all builds
-- Pack current (A) — **default field build** (`ENABLE_ACS758 0`, `BENCH_ONLY 0`): INA228 `readCurrent()` through external precision shunt; **alternative field build** (`ENABLE_ACS758 1`): ACS758LCB-200B-PFF-T Hall-effect sensor on A1; **bench build** (`BENCH_ONLY 1`): INA228 onboard 15 mΩ shunt via `readCurrent()` (≤ 10 A only)
-- Pack housing temperature (°C) — NTC thermistor on A0
-- Cell-group voltages in mV (up to 4 per frame from the placeholder parser) — optional CAN BMS
-
-**Computed** (per wake, from collected + persisted state):
-- SoC (%) — voltage-based OCV lookup, linearly interpolated; no temperature compensation (see [§10](#10-limitations-and-next-steps))
-- Cycle Ah throughput — sampled bidirectional Ah accumulator (`|current| × Δt` once per wake, 0.5 A noise floor); counts both charge and discharge current; resets when the pseudo-cycle SoH threshold triggers. **Note:** this is not depth of discharge; it does not track the net discharged deficit below full. See §10 for the production Coulomb-counting extension.
-- SoH (%) — EWMA of discharge-only Ah accumulated per pseudo-cycle divided by `rated_cap_ah`; updated when SoC crosses the 30% → 90% heuristic threshold
-
-**Transmitted:**
-- `battery_status.qo` — one record per `report_interval_m` (default 24 records/day), queued and flushed at the Notecard's hourly outbound sync. Contains window-averaged pack_v, cur_a, temp_c, plus the current SoC, SoH, cycle Ah throughput accumulator (`throughput_ah`), and CAN health flag (`can_ok` is `false` by design when `ENABLE_CAN_BMS=0`, not a fault). Compact-encoded; deleted if satellite session is established. Example body:
-
- ```json
- { "pack_v": 48.2, "cur_a": 12.4, "temp_c": 28.5, "soc_pct": 74, "soh_pct": 88, "throughput_ah": 42.1, "can_ok": true }
- ```
-- `battery_alert.qo` — emitted only on a threshold trip. All alert types except `can_error` carry `sync: true` to bypass the outbound timer and transmit immediately over cellular or Skylo satellite, whichever is available. `can_error` queues for normal outbound delivery (see below). Per-alert 30-minute cooldown prevents repeated paging.
-
-**Alert triggers:**
-
-All alert Notes share a fixed compact schema with five fields always present: `alert` (string), `pack_v` (V), `soc_pct` (%), `temp_c` (°C; `-9999.0` if no valid thermistor read at fire time), and `extra_v` (float, always emitted, `0.0` when no auxiliary value applies). Downstream consumers should expect all five fields on every alert Note.
-
-| Alert Type | Trigger | `extra_v` value | Notes |
-|---|---|---|---|
-| `soc_low` | SoC drops below `soc_alert_pct` | `0.0` | Example: `{"alert":"soc_low","pack_v":43.1,"soc_pct":18,"temp_c":27.3,"extra_v":0.0}` |
-| `temp_high` | Temp exceeds `temp_high_c` | Temperature at trip (°C) | Same as `temp_c`. Lithium loses capacity above ~45°C; runaway risk above 60°C. |
-| `temp_low` | Temp falls below `temp_low_c` | Temperature at trip (°C) | Lithium must not charge below 0°C. |
-| `soh_low` | Rolling SoH drops below `soh_alert_pct` | Current SoH (%) | Signal to schedule pack inspection or replacement. |
-| `cell_imbalance` (CAN) | Max–min cell voltage exceeds `cell_delta_mv` | Voltage delta (mV) | `pack_v` = `0.0`, `temp_c` = `-9999.0` (CAN poll doesn't re-sample V/I/T). |
-| `can_error` (CAN) | No CAN frame received during poll window | `0.0` | Fires on first failure; suppressed 1 hour after each emission. `pack_v` = `0.0`, `temp_c` = `-9999.0`. Queues for normal outbound (not `sync:true`). |
-
-**Routing:** Notehub fans `battery_alert.qo` to a real-time notification channel and `battery_status.qo` to a long-term analytics store for cycle trending and fleet-level SoH dashboards. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for setup details — this project ships no specific downstream endpoint.
-
-## 9. Validation and Testing
-
-**Expected cadence.** In steady state on a healthy machine, expect one `battery_status.qo` per hour and zero `battery_alert.qo` events. SoH will start at `100%` and update toward the true measured value after the first full discharge/charge cycle; budget a few days of normal operation before the SoH number stabilizes. Alert threshold trips during the first week are likely the result of default thresholds not matching the specific pack; tune via environment variables rather than reflashing.
-
-**Bench bring-up.** Before connecting to a live machine:
-1. Power the Notecarrier CX + Notecard from a bench supply at 5V through the Mojo.
-2. Connect the bench supply positive to INA228 `VIN+` (the shunt high side); connect INA228 `VIN–` to a bench electronic load set to 1–2 A constant current; return the electronic load to the supply GND = INA228 `GND`. Set the supply to 48 V. An electronic load is far safer and more controllable than a power resistor here: 48 V at 5 A is 240 W and a resistive load rated for that power becomes dangerously hot. If only a resistor is available, select at least a 50 W chassis-mount unit and keep the test brief.
-3. Check the serial output at 115200 baud: look for `[meas]` lines reporting reasonable `pack_v` and `cur_a` values, then verify the first `battery_status.qo` in Notehub within the first `report_interval_m` minutes.
-4. Drop `soc_alert_pct` to a high value (e.g., `95`) via the Fleet environment to force an immediate alert — confirm `battery_alert.qo` appears in Notehub with `sync: true` latency (typically 15–60 seconds from cellular session establishment).
-
-**Validating power behavior with Mojo.** The Blues Mojo contains an LTC2959 coulomb counter accurate to 1% over its measurement range. For a whole-device measurement, use the bench wiring in [§5](#5-wiring-and-assembly): splice Mojo so it intercepts all 5V loads — the Notecarrier CX VBAT+ pad and, when the CAN subsystem is installed, the CAN module VCC and TXB0104 VCCB supply lines. With all loads downstream of Mojo the deep-sleep current shown in the trace includes the INA228, thermistor divider, buck regulator, and optional CAN hardware in addition to the Notecard itself. The assembled idle figure will therefore be materially higher than the Notecard's published 8–18 µA alone — use Mojo to determine the actual number for your specific build rather than relying on the datasheet figure. Per-state current figures (Notecard rows from the [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/); host-awake and CAN rows estimated from component specs, verify with Mojo):
-
-| Firmware state | Current @ 5V |
-|---|---|
-| Deep sleep (host off, Notecard idle between sessions) | ~8–18 µA — Notecard alone ([NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)). Assembled device draws materially more: INA228, thermistor divider, and buck regulator each contribute quiescent current. **Measure the assembled idle with Mojo** to determine the actual figure for your specific build. |
-| Host awake, sampling (INA228 I²C + ADC read, ~2–3 seconds) | **Initial estimate ~15–25 mA at the 5V rail** (derived from Cygnet STM32 active-run + INA228 + thermistor divider + buck-regulator quiescent specs; not bench-measured). Add ~5–10 mA if `ENABLE_ACS758 1` (ACS758 VCC load at 5 V). **Measure with Mojo before deployment** — actual assembled current varies with PCB layout, regulator selection, and peripheral choices. |
-| CAN polling (when ENABLE_CAN_BMS=1, CAN module powered from Mojo LOAD output) | **Initial estimate ~10–15 mA additional** above the host-awake baseline (derived from MCP2515 + TJA1050 + TXB0104 datasheet quiescent figures; not bench-measured). **Measure with Mojo** with the full CAN subsystem downstream of the Mojo LOAD pin before relying on this number. |
-| Cellular session (LTE-M outbound sync) | ~250 mA average — Notecard modem active ([NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)). The NOTE-NBGLWX is LTE-M/NB-IoT/GPRS — no GSM 2 A burst. Add assembled idle draw for the session duration. |
-| WiFi session (when WiFi link is used) | ~80 mA average — Notecard WiFi active. Add assembled idle draw for the session duration. |
-| Satellite NTN session (Skylo) | Modem active during NTN acquisition and transmit. See [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for confirmed NTN-mode current figures. Session establishment typically 1–3 min. |
-
-**Determining 24-hour energy consumption.** Use Mojo to measure the integrated charge over one complete sample cycle (deep sleep + host wake + any radio activity) and one complete cellular session separately. With firmware defaults (5-minute sample, hourly cellular sync), the 24-hour total is approximately: (sleep + wake charge per cycle × 288 cycles/day) + (cellular session charge × 24 sessions/day). Do not use estimated per-component figures for this calculation — the assembled quiescent current varies significantly with buck-regulator selection, PCB routing, and peripheral choices. Measure the actual assembled build with Mojo before specifying battery backup capacity. If the Mojo trace shows a continuous elevated baseline rather than brief wake pulses, the host is not sleeping — confirm `NotePayloadSaveAndSleep` is not returning early (see [card.attn docs](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn)). Hourly bursts that run 2 minutes or longer typically indicate marginal cellular signal; move the antenna or check Skylo coverage at the test site.
-
-Satellite sessions consume an order of magnitude more energy per kilobyte than LTE-M. Since summaries are suppressed over satellite (`delete: true` on the template), the satellite path should only fire alert-sized payloads, typically under 100 bytes each. One or two alerts per satellite session is an acceptable budget for a critical low-SoC event; a session that sends many alerts in rapid succession suggests alert dedup is not working and the cooldown should be verified.
-
-Mojo is not deployed to the field unit; its role ends when a firmware revision passes the bench trace check.
-
-### Common bench issues
-
-| Symptom | Likely cause | What to check |
-|---|---|---|
-| Device never appears in Notehub **Devices** tab. | `PRODUCT_UID` wrong or empty; antenna not outdoors. | Re-verify `PRODUCT_UID`. Move the unit outdoors with antenna pointed up. Confirm `_session.qo` events — none means no radio connection. |
-| `_session.qo` appears but no `battery_status.qo`. | First `report_interval_m` has not elapsed, or `summ_count` stayed zero because all sensor reads were rejected. | Check serial at 115200 baud for `[meas]` lines. Confirm INA228 is powered and `pack_v` is non-zero. |
-| `pack_v` reads ~0 V or firmware reports INA228 not found. | INA228 not found at address 0x40, or SDA/SCL not connected. | Verify Qwiic continuity. Issue `{"req":"card.status"}` in the [In-Browser Terminal](https://dev.blues.io/terminal/) to confirm the Notecard is alive, then check INA228 I²C wiring. |
-| `pack_v` reads out of plausible range (>85 V or <10 V). | INA228 `VIN+` not connected to the pack positive terminal, or wiring reversed; or pack voltage exceeds the INA228's 85 V common-mode limit. | Confirm the `VIN+` wire reaches the pack positive with a DMM. Verify `GND` is at pack negative. For packs whose full-charge voltage may exceed 85 V, substitute a voltage-sensing front end with a higher common-mode rating (see §10). |
-| `temp_c` is `-9999.0` with otherwise valid readings. | NTC thermistor unplugged or series resistor missing. | Check continuity at A0 to the NTC junction. With a 10 kΩ NTC at room temperature, A0 should read ~1.65 V. |
-| `soc_pct` shows 0% with a full pack. | Wrong `chemistry` env var for this pack type. | Check the `chemistry` variable in Notehub. A lithium OCV table applied to a lead-acid pack reads incorrectly because the voltage ranges differ by several volts. |
-| CAN data always absent (`can_ok: false`). | `ENABLE_CAN_BMS` is 0 (default), or `BMS_CELL_GROUP_ID` doesn't match the machine. | Set `ENABLE_CAN_BMS` to `1` in `lift_battery_monitor_config.h` and recompile. Use a CAN analyzer to confirm the BMS is actually transmitting and capture the correct CAN ID. |
-| Alerts fire every sample with no cooldown. | Alert cooldown state not persisted — `PersistState` seg ID conflict or struct too large. | Verify `STATE_SEG_ID` is unique (currently `"LIFT"`). Confirm the `PersistState` struct has not grown past the Notecard flash payload limit. |
-
-## 10. Limitations and Next Steps
-
-A pack-health monitor has to make decisions about safety-critical equipment, so the boundary between this reference build and a production fleet rollout is well-defined: shunt sizing has to match real traction currents, SoC needs both OCV and Coulomb counting to be trustworthy under load, CAN integration is per-vendor work, and the device has to stay powered when the machine is in storage, the very interval where dead-on-arrival problems develop. The list below names each of those boundaries and points at the natural production path for each.
-
-### Simplified for this Proof-of-Concept
-
-The simplifications below are scope choices: each names a boundary where a production fleet rollout will want a different shunt, a blended SoC estimate, vendor-specific CAN work, or an always-on power feed.
-
-**The INA228 onboard shunt is bench-only; the default production path uses an external precision shunt.** Electric aerial lifts commonly draw 50–200 A during lift operation; the INA228's onboard 15 mΩ shunt is rated to ~10 A continuous and **must not** be placed inline on a traction-pack conductor at those currents. The firmware ships with `ENABLE_ACS758 0` as the default: `readPackVI()` reads pack current via INA228 `readCurrent()` through an external precision shunt wired to `VIN+`/`VIN–` (see §4 and §5). Update `DEFAULT_SHUNT_MOHM` and `DEFAULT_SHUNT_MAX_A` to match the installed shunt before building. The alternative field path (`ENABLE_ACS758 1`) uses an ACS758 Hall-effect sensor on A1 instead; use this path when galvanic isolation or inline shunt installation is impractical. For bench testing only, set `ENABLE_ACS758 0` and `BENCH_ONLY 1` to use the onboard 15 mΩ shunt at ≤10 A. The INA228's bus-voltage path (up to 85 V common-mode) is unaffected by shunt configuration in all cases.
-
-**OCV-based SoC is inaccurate under load.** Open-circuit voltage corresponds to SoC only when the pack is at rest (no load or charge current) for at least several minutes. During operation the terminal voltage is depressed by the internal resistance drop; the firmware's SoC estimate will read lower than actual SoC while the lift is in motion. For production, blend the OCV lookup with a Coulomb-counting depth-of-discharge estimate. Coulomb counting is accurate during dynamic operation; OCV is accurate at rest. The `throughput_ah` field is a bidirectional accumulator, not a true DoD value; a production Coulomb counter must separately track the net discharged deficit below full charge.
-
-**SoC tables are sized for 48V packs.** The lookup tables in the firmware assume a 16S LiFePO4 or 24-cell lead-acid 48V pack. Machines with 24V, 36V, 72V, or 80V packs need tables scaled to their cell count. A `rated_voltage_v` environment variable and a scaling pass on the OCV table entries is the clean production extension. **Note: 72V and 80V packs also require verifying the INA228 common-mode limit before deployment. See below.**
-
-**INA228 bus-voltage input is limited to 85 V common-mode.** The Adafruit INA228 breakout used for bus-voltage sensing has an 85 V common-mode input limit. This is comfortably above the full-charge voltage of the 48 V-class packs the firmware defaults target (16S LiFePO4 full charge ≈ 58.4 V; 24-cell lead-acid ≈ 51.8 V). However, higher-voltage packs leave materially less headroom: a 20S LiFePO4 72 V-nominal pack can reach up to 73 V fully charged, still within limits; but a 22S or 24S LiFePO4 pack marketed as "72 V" may reach 80–87.6 V fully charged depending on cell count. Many 80 V-class packs (e.g. 24S LiFePO4 at 3.65 V/cell = 87.6 V) will **exceed** the 85 V INA228 limit when fully charged, which could damage the device. **Before deploying this design on any pack above 60 V nominal, measure or calculate the full-charge bus voltage and confirm it remains below 85 V.** For packs that cannot meet this constraint, substitute a voltage-sensing front end with a higher common-mode rating (such as an isolated bus-voltage monitor) in place of the INA228 for the pack-voltage measurement; the rest of the architecture is unaffected.
-
-**CAN BMS integration is a placeholder, not deployable without vendor-specific work.** The CAN frame parser ships with a hardcoded example `BMS_CELL_GROUP_ID` of `0x18FF50E5` (a 29-bit extended J1939-style ID), a fixed 250 kbps bitrate, single-frame parsing only, and a simple layout that reads however many 2-byte groups fit in one frame. This placeholder will not match any real BMS without customization. Real BMS vendors use proprietary CAN IDs, scaling factors, multi-frame protocols, and message formats. The developer must obtain the BMS vendor's CAN DBC file or protocol documentation, update `BMS_CELL_GROUP_ID` and `parseCellGroupFrame()` to match, and verify the bitrate against the machine's CAN bus configuration. Some rental-equipment BMS implementations use SAE J1939 PGNs; others use entirely proprietary protocols with no public documentation. **Do not deploy `ENABLE_CAN_BMS 1` to a machine without first capturing and decoding the machine's actual CAN traffic with an analyzer.**
-
-**SoH requires a full discharge cycle to calibrate.** On first power-on, SoH initializes at 100%. The first meaningful SoH reading arrives after the device has observed one complete discharge cycle down below 30% SoC followed by a charge to above 90% SoC. Until then, the field is present in summaries but may be misleading.
-
-**No temperature compensation on OCV.** Battery OCV shifts with temperature: a cold lead-acid pack at 0°C reads several tenths of a volt lower at the same SoC as at 25°C. The firmware does not apply a temperature correction to the OCV lookup. For accurate low-temperature SoC on lead-acid packs, add a ΔV/ΔT correction factor; the pack temperature is already available in firmware.
-
-**Aux-rail power dependency.** The design powers the monitor from the machine's 12 V auxiliary circuit. On machines where that rail drops when the key is turned off, the charger is disconnected, or the master disconnect is opened, the monitor goes blind during storage, transport, and between-job intervals, precisely when low SoC, deep discharge, and thermal excursions are most likely to develop undetected. The §4 commissioning check addresses this, but if the field team has not confirmed that the chosen aux rail is always-on, the monitor provides no coverage during machine storage. For full coverage, the power feed must come from the main traction pack terminals through an appropriately rated HV DC/DC stage.
-
-**Satellite data budget.** Notecard for Skylo includes 10 KB of satellite data. Each compact alert Note is typically under 100 bytes on the wire; 10 KB covers roughly 100 satellite alert events per billing period. Deployments where sustained NTN operation is expected should monitor satellite usage in Notehub and consider upgrading the data plan. Skylo coverage also requires an unobstructed view of the equatorial sky; the antenna must be mounted on the machine exterior, not in an enclosed battery bay.
-
-**Mojo does not read from the Notecard in firmware.** The sketch does not query the LTC2959 coulomb counter over Qwiic for runtime power telemetry. Adding a `mojo_mah` field to the summary is a straightforward extension.
-
-### Production Next Steps
-
-Once the basic monitor is on a machine, the following extensions harden it for a real rental fleet, roughly from the most immediately useful to the most integration-dependent.
-
-**Commission the current sensor on each deployed unit.** For the default external-shunt field path (`ENABLE_ACS758 0`): confirm `DEFAULT_SHUNT_MOHM` and `DEFAULT_SHUNT_MAX_A` in the firmware match the installed shunt before flashing. After installation with the machine powered but the main contactor open, check the `[meas] i=` line in the serial output — it should read near 0 A. For the ACS758 alternative path (`ENABLE_ACS758 1`): the firmware ships with nominal datasheet values (`acs758_zero_v` = 2.5 V, `acs758_mv_per_a` = 10.0 mV/A), but Hall-sensor zero-offset error can bias the Ah accumulator by several amps and corrupt throughput and SoH over time. After installation with zero traction current flowing: check the `[meas] i=` line and, if the offset is significant, calculate the true zero-current `VOUT = analogRead(A1) × 3.3/4095 × 1.5` and set the `acs758_zero_v` Notehub environment variable to that measured value. If the installed sensor variant differs from ACS758LCB-200B (10 mV/A), update `acs758_mv_per_a` accordingly.
-
-**Add temperature compensation** to the OCV lookup table and to the Coulomb counting (charge efficiency degrades at low temperature).
-
-**Expand the SoC table** to cover all common pack voltages (24V, 36V, 48V); for 72V and 80V packs, first verify or replace the INA228 voltage-sensing front end (see the INA228 common-mode limit Note above), then add the higher-voltage table entries and drive selection from a `rated_voltage_v` environment variable.
-
-**Implement vendor-specific CAN BMS parsers** for the top rental-fleet BMS manufacturers (e.g., Zivan, Delta-Q, Enersys).
-
-**Add a `battery_command.qi` inbound Notefile** for fleet-triggered actions such as "reset SoH accumulator after a pack swap" or "clear alert cooldowns after a maintenance visit."
-
-**Deploy [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** so the rental company can push OCV table updates, threshold recipes, and new BMS parsers across the fleet without a service visit.
-
-**Store a rolling history** of the last N cycle SoH measurements in the summary Note to give the fleet dashboard a degradation trend rather than a single current value.
-
-## 11. Summary
-
-The rental scissor lift that used to arrive at a job site with a quietly failing pack now reports its own health every hour. A precision I²C power monitor, a thermistor, and an optional CAN BMS interface turn the pack from an opaque black box into a continuously-monitored asset; sampling runs locally every five minutes; transmission runs hourly over LTE-M with Skylo satellite as the automatic fallback when the lift is on a site with no cell coverage. Threshold trips (low SoC before a job-site delivery, a thermal excursion in storage, SoH degradation approaching end-of-life) reach the rental company's fleet desk within seconds, over whatever radio path the machine can reach. Dead-on-arrival deliveries become a diagnosable, preventable event instead of a $2,000 emergency swap and an angry customer. The same architecture extends cleanly to any electric machine in a rental fleet whose pack fully-charged bus voltage remains within the INA228's 85 V common-mode limit — compact forklifts, electric floor scrubbers, pallet jacks, utility vehicles — with only the OCV lookup tables and CAN BMS parser changing per machine type. Higher-voltage variants (packs above ~60 V nominal where the full-charge voltage may approach or exceed 85 V) need a voltage-sensing front end with a higher common-mode rating in addition to the table and parser updates.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/aerial-lift-rental-equipment-battery-health-monitor/)**
diff --git a/82-aerial-lift-rental-equipment-battery-health-monitor/banner.png b/82-aerial-lift-rental-equipment-battery-health-monitor/banner.png
deleted file mode 100644
index 2ca5688a..00000000
Binary files a/82-aerial-lift-rental-equipment-battery-health-monitor/banner.png and /dev/null differ
diff --git a/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/01-system-architecture.svg b/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index 7d632e15..00000000
--- a/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/02-wiring-assembly.svg b/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 7eca51c7..00000000
--- a/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,75 +0,0 @@
-
diff --git a/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/03-data-flow.svg b/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index 3f74a0d5..00000000
--- a/82-aerial-lift-rental-equipment-battery-health-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,61 +0,0 @@
-
diff --git a/83-off-grid-solar-battery-site-controller/README.md b/83-off-grid-solar-battery-site-controller/README.md
index 470dd7b0..3e01c8f9 100644
--- a/83-off-grid-solar-battery-site-controller/README.md
+++ b/83-off-grid-solar-battery-site-controller/README.md
@@ -1,556 +1,5 @@
-# Off-Grid Solar + Battery Bank Monitor
+# Off-Grid Solar Battery Site Controller
-
+A cellular reference design for remote off-grid sites that reads Victron VE.Direct devices and reports battery state of charge, daily solar harvest, and load draw to Notehub every four hours.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Off-Grid%20Solar%20%2B%20Battery%20Bank%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/solar_battery_controller/](firmware/solar_battery_controller/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a bank-level solar and battery monitoring solution — a Blues [battery management systems](https://blues.com/battery-management-systems/) reference design — for remote off-grid sites powered by solar arrays and battery banks. A Blues Notecarrier CX reads two Victron VE.Direct devices — a SmartShunt for battery-bank metrics and a SmartSolar MPPT charge controller for solar-side metrics, and reports battery-bank-level state of charge (SoC), daily solar harvest, load draw, battery temperature, and charge state to the [Blues Notehub](https://blues.com/notehub/) cloud service every four hours, with immediate alerts before the site goes dark. Connectivity is provided by a Blues Notecard seated in the carrier's M.2 slot; two SKUs are supported with no firmware changes between them — [Notecard Cell+WiFi (NOTE-MBGLW)](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) for sites within terrestrial cellular range, and [Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for fallback satellite coverage for sites where cellular coverage is marginal or absent.
-
-## 1. Project Overview
-
-**The problem.** Remote sites that run on solar and battery — cell towers at the edge of coverage, environmental monitoring stations in the backcountry, off-grid cabins — share a common failure mode: a problem that started small, days ago, accumulates unnoticed until the site goes dark. A slowly degrading PV array, a load that crept up after a firmware update to on-site equipment, or a battery bank whose capacity has quietly faded with age: none of these are catastrophic on their own, but any one of them can drain a bank that the solar array is no longer sized to replenish. This design detects that condition directly: if the charge controller fails to reach a full-charge state (Float, Absorption, Equalize, or Auto Equalize) for a configurable number of consecutive days, a `harvest_deficit` alert fires before the bank is depleted — giving the operations team time to respond before the site goes dark. And because the site is, by definition, remote, a days-long recharge deficit is invisible without continuous telemetry.
-
-
-
-**Interface Note.** This project reads the [Victron VE.Direct text protocol](https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.34.pdf) — a one-wire broadcast interface that exposes battery-bank aggregates (SoC, voltage, current, temperature, and charge state) and solar-side metrics (panel voltage, power, and daily yield). These are exactly the signals needed to detect the site-uptime failure modes this design targets: persistent recharge deficit, low SoC, thermal overtemperature, and excessive load draw. Bank-level aggregates are the correct scope for a bank-level site-uptime monitor. **Cell-imbalance monitoring was explicitly evaluated during design and scoped out**: per-cell voltages and imbalance data are not broadcast on the VE.Direct wire — they travel over CAN bus and require a dedicated CAN controller and transceiver that are absent from this hardware stack. Implementing cell-level telemetry is a distinct hardware and firmware problem that belongs in a companion design rather than an extension of this project (see §10 for the full rationale and the companion-design specification).
-
-
-
-**Why Notecard.** These sites are by definition off-grid *and* off-network. There is no building WiFi to connect to, no Ethernet jack in the enclosure, and no cellular router whose monthly bill someone else is paying. The Blues Notecard self-manages its radio session, draws microamp-range idle current between transmissions, and requires no site IT involvement to set up. Two SKUs seat in the same M.2 slot on the Notecarrier CX and run the same firmware without modification:
-
-
-
-- **[Notecard Cell+WiFi (NOTE-MBGLW)](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/):** LTE Cat-1 bis cellular with opportunistic WiFi fallback. The practical choice for bench validation and sites with adequate terrestrial coverage.
-- **[Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/):** LTE-M cellular combined with [Skylo](https://www.skylo.tech/) Non-Terrestrial Network (NTN) satellite connectivity on a single card. The Notecard manages radio-mode selection internally; no firmware differences exist between the two SKUs. **Both antennas must be positioned outdoors with an unobstructed view of the sky**. See §4 and §5 for the mounting and enclosure-feedthrough requirements. Notecard for Skylo is the primary production SKU for the truly remote towers, wilderness arrays, and high-altitude installations this use case targets.
-
-**Deployment scenario.** A Notecarrier CX mounted inside the existing site enclosure or a weatherproof addon box, powered from the site's 5V regulation bus or a small DC-DC converter off the main battery bus. Two short VE.Direct cables run from the Notecarrier CX dual-row header to the SmartShunt (battery shunt, usually mounted near the battery bank negative terminal) and to the SmartSolar MPPT controller (typically mounted on the enclosure wall). No changes to the Victron equipment, no interruption to the solar system.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Almost all of the time the Cygnet STM32L433 host on the Notecarrier CX is off — not idle, off. Every 15 minutes (configurable) it powers up, reads one VE.Direct frame from each Victron device over UART, accumulates rolling averages in a [`NotePayload`](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) state struct that the Notecard holds in flash between sleep cycles, evaluates four alert rules locally, and goes back to sleep. A full cycle takes under 8 seconds. The Notecard rides on I²C, so there are no serial buffers, no AT commands, and no modem state machine cluttering the host firmware.
-
-**Notecard responsibilities.** The Notecard does the connectivity work the host never sees. It holds outbound [Notes](https://dev.blues.io/api-reference/glossary/#note) in its on-device queue, wakes the radio on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 4 hours), and breaks that cadence the moment a `sync:true` alert lands in the queue; those flush immediately. It also pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) down from Notehub on each inbound sync, so alert thresholds and sample intervals can be retuned without a firmware update or a site visit.
-
-**Notehub responsibilities.** On the far end, the Notecard's embedded global SIM negotiates with whatever carrier reaches the site, and [Notehub](https://notehub.io) ingests every event that arrives. Alerts (`solar_alert.qo`) and summaries (`solar_summary.qo`) land in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile), which means routes can fan them out at different urgencies with no filter logic: alerts straight to the NOC or on-call system, summaries to a long-term analytics store for trend work.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other targets; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-To get a working monitor up and running in ~2 hours:
-
-1. **Assemble** the Notecarrier CX with Notecard (Cell+WiFi or Skylo), DC-DC converter, and resistor dividers per §4.
-2. **Wire** the SmartShunt and MPPT to the two VE.Direct ports on the Notecarrier CX header (§4).
-3. **Install dependencies:**
- ```bash
- arduino-cli core install "STMicroelectronics:stm32"
- arduino-cli lib install "Blues Wireless Notecard"
- ```
-4. **Set ProductUID** in `firmware/solar_battery_controller/solar_battery_controller.ino` line 62.
-5. **Flash:**
- ```bash
- arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/solar_battery_controller/
- arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/solar_battery_controller/
- ```
-6. **Claim and configure in Notehub:**
- - Power on; the device claims itself to your project within ~1 minute (watch **Devices** tab).
- - Create a Fleet; set **Fleet → Environment** variables (see table in §5) — defaults are reasonable for initial testing.
- - Add routes for `solar_alert.qo` (real-time) and `solar_summary.qo` (storage/analytics).
-7. **Validate:** you should see `_session.qo` and `solar_summary.qo` Notes appearing in Notehub within 5–10 minutes (see "What you should see in Notehub" in §5).
-
-
-Here is a sample Note this device emits:
-
-```json
-{
- "bat_v": 26.1,
- "bat_a": -3.2,
- "bat_w": -83.5,
- "soc_pct": 78.4,
- "bat_temp_c": 22.1,
- "pv_v": 34.8,
- "pv_w": 142.0,
- "load_w": 225.5,
- "yield_kwh": 1.24,
- "ttg_min": -1,
- "cs": 5
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host — no separate MCU. Provides the UART (Serial1), analog, and SPI/I²C headers needed for this sensor mix. |
-| [Notecard Cell+WiFi (NOTE-MBGLW)](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) | 1 *(bench / sites with reliable cellular coverage)* | LTE Cat-1 bis cellular with opportunistic WiFi fallback. Prepaid global SIM, no monthly commitment. Use this SKU for bench validation and field sites where terrestrial cellular coverage is reliable. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) | 1 *(sites with marginal or no terrestrial coverage)* | LTE-M cellular combined with Skylo NTN satellite on a single M.2 card — the production SKU for remote towers, backcountry arrays, and high-altitude installations where terrestrial cellular is unreliable. The Notecard-certified satellite/LTE antenna and a passive GPS/GNSS antenna are both required and **must be mounted outdoors with an unobstructed view of the sky** (see §5 for enclosure feedthrough guidance). No firmware changes vs. the NOTE-MBGLW. See [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) and [antenna guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/) before deploying. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter on the +VBAT rail for validating the sleep/wake current profile during commissioning and bench bring-up. See [§9](#9-validation-and-testing). |
-| [Victron SmartShunt 500A/50mV](https://www.victronenergy.com/battery-monitors/smart-battery-shunt) | 1 | Industry-standard battery shunt monitor. Measures battery voltage, current, SoC, and optional temperature via VE.Direct UART. The 500A variant covers the vast majority of off-grid installations from small cabins to telecom sites; a 1000A variant is available for high-current systems. Includes a VE.Direct cable. |
-| [Victron SmartSolar MPPT 75/15](https://www.victronenergy.com/solar-charge-controllers/smartsolar-mppt-75-10-75-15-100-15-100-20) | 1 *(or existing)* | Solar charge controller. Exposes panel voltage, instantaneous panel power, daily yield, and charge state over VE.Direct. Select the MPPT SKU that matches your array size; the VE.Direct protocol and field names are the same across the SmartSolar family. Includes a VE.Direct cable. |
-| [Victron VE.Direct cable](https://www.victronenergy.com/cables/ve.direct.cable) | 0–2 | 4-pin JST PH 2.0 to bare-wire cable. One included with each Victron device above; purchase extras if your layout requires longer runs. **Wire colors are not standardized — always verify pin assignments with a multimeter before connecting** (see §5). |
-| 10 kΩ, 20 kΩ resistors | 2 each | One voltage-divider pair per VE.Direct RX line: device TX → 10 kΩ → MCU RX pin; junction → 20 kΩ → GND. Output ≈ 3.33 V. VE.Direct is a one-way broadcast protocol — the host only reads, never writes, so a simple resistor divider is the correct and lowest-risk interface for this unidirectional UART application. See [§5](#5-wiring-and-assembly) for the pin-by-pin wiring. |
-| Victron Temperature Sensor for SmartShunt | 0–1 | Plugs into the SmartShunt's temp sensor port; required to populate `bat_temp_c` in the summary Note. Optional but strongly recommended for lithium battery banks where thermal runaway is a real risk. |
-| [Traco TSR 1-2450](https://www.tracopower.com/int/model/tsr-1-2450) DC-DC converter *(12 V and 24 V sites)* | 1 | 6.5–36 V input, 5 V / 1 A output, SIP-3 package. Input range spans 12 V and 24 V battery buses through their full charge/discharge swing. See the [TSR 1 datasheet](https://www.tracopower.com/int/wp-content/uploads/sites/2/documents/tsr1.pdf) for derating above 40 °C. |
-| [Traco TSR 1-4850](https://www.tracopower.com/model/tsr-1-4850wi) DC-DC converter *(48 V telecom sites)* | 1 | 18–75 V input, 5 V / 1 A output, same SIP-3 footprint as the TSR 1-2450. Covers 48 V telecom battery buses at full charge. Substitute this part for the TSR 1-2450 on 48 V deployments; no other changes required. |
-| [Hammond 1555JGY](https://www.hammfg.com/part/1555JGY) weatherproof enclosure | 1 | 125 × 95 × 57 mm, ABS, IP67, gray. Fits the Notecarrier CX, resistor divider components, and DC-DC converter. Mount inside the site's existing primary enclosure or alongside the SmartShunt. |
-| Skylo-certified cellular/NTN antenna — **included with the NOTE-NBGLWX kit** *(MAIN u.FL port)* | 1 *(NOTE-NBGLWX only)* | Ships in the NOTE-NBGLWX kit; connects to the `MAIN` u.FL port. **Do not substitute** — the Notecard is certified on Skylo's network exclusively with this antenna; replacing it renders the device uncertified and Skylo may block it from the network. If a different antenna is required, a Skylo delta-certification test is needed; contact [Blues](https://blues.com/contact-sales/) for recommended test houses. Must be mounted outdoors with an unobstructed view of the sky (see §5). |
-| [Blues Flexible Dual LTE/Wi-Fi and GPS/GNSS Antenna (Quectel YCA001BA)](https://shop.blues.com/products/dual-flexible-antenna-cell-wi-fi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(NOTE-NBGLWX GPS port)* | 1 *(NOTE-NBGLWX only)* | Passive flexible multi-band antenna with u.FL connector; GNSS L1 coverage (1560–1620 MHz, including GPS L1 at 1575 MHz) makes it suitable for the `GPS` u.FL port on the NOTE-NBGLWX. The NOTE-NBGLWX must acquire a GPS fix before each satellite session — without a GNSS antenna, NTN transmission cannot occur. **Use a passive (un-amplified) antenna only** on this port; active/LNA GNSS antennas are not compatible with the NOTE-NBGLWX GPS input. Route the flexible cable lead through a cable gland in the enclosure wall and mount flat on the enclosure exterior with a clear sky view. |
-| u.FL to SMA female pigtail, ≥100 mm, RG316 or RG178 (e.g. [SparkFun WRL-09145](https://www.sparkfun.com/products/9145)) + IP67-rated SMA bulkhead connector *(NOTE-NBGLWX MAIN feedthrough)* | 1 *(NOTE-NBGLWX only)* | Routes the included Skylo antenna from the Notecard's `MAIN` u.FL port through the enclosure wall via an SMA bulkhead connector. Use the shortest pigtail that reaches the nearest enclosure wall without sharp bends to minimize insertion loss on the satellite band. IP67-rated SMA panel-mount bulkhead connectors are available from DigiKey and Mouser. The GPS antenna (Quectel YCA001BA above) has a flexible lead long enough to route directly through its own cable gland — no additional pigtail is required for the GPS port. |
-| M16 IP67 cable gland, suitable for 4–8 mm cable OD *(NOTE-NBGLWX GPS feedthrough)* | 1 *(NOTE-NBGLWX only)* | Seals the GPS antenna (Quectel YCA001BA) cable lead where it passes through the enclosure wall. The MAIN antenna routes through the SMA bulkhead connector listed above, which provides its own weather seal. Thread and bore size must match your enclosure wall thickness; nylon M16 glands are available from DigiKey, Mouser, and electrical distributors. |
-| Cellular/LTE antenna with u.FL connector (see [Blues antenna guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/) for certified options) | 1 *(NOTE-MBGLW)* | A u.FL cellular antenna must be connected to the NOTE-MBGLW's `MAIN` u.FL pigtail lead on the Notecarrier CX — the carrier has no onboard PCB trace cellular antenna. A compact flexible u.FL LTE antenna is adequate for non-metallic (plastic or ABS) enclosures; for metallic enclosures add a u.FL-to-SMA pigtail and an IP67-rated SMA bulkhead connector to route the antenna outside the enclosure wall. See the [Blues antenna guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/) for compatible antenna selection and placement guidance. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service: no activation fees, no monthly commitment.
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O uses the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual-row 16-pin header. The Notecard (Cell+WiFi or Skylo. See §4) seats into the carrier's M.2 slot. The Mojo sits inline on the +VBAT power rail for bench bring-up and commissioning. VE.Direct TX-to-MCU level shifting is handled by a simple 10 kΩ/20 kΩ resistor divider on each RX line. This is the only documented and validated interface for this project; active level-shifter boards with bidirectional MOSFETs are not recommended for this unidirectional UART application.
-
-
-
-**Safety — high-current DC systems.** Off-grid battery banks can source thousands of amps into a short circuit. Before making any connections in or near battery, shunt, or MPPT wiring: disconnect the solar array at the MPPT PV input terminals; verify polarity at every connection point with a multimeter before making contact; and have all DC wiring reviewed by a qualified installer before commissioning. The VE.Direct signal connections below are low-voltage and carry microamp-level current — they do not interrupt the high-current path, but they pass through the same enclosure as live battery wiring.
-
-
-
-### VE.Direct cable pinout
-
-Each Victron device exposes a 4-pin JST PH 2.0 (2.0 mm pitch) port for VE.Direct. The standard pin numbering, with pin 1 at the locking-tab end of the connector, is:
-
-| Pin | Signal | Direction | Notes |
-|---|---|---|---|
-| 1 | GND | — | Connect to Notecarrier CX GND |
-| 2 | RX (into device) | Host → Device | Leave NC — protocol is device-broadcast only |
-| 3 | TX (out from device) | Device → Host | 5V logic; needs level shifting or divider |
-| 4 | +5V (from device) | Device → Host | Leave NC — host is externally powered |
-
-
-
-**Wire color warning.** Wire colors on Victron VE.Direct cables are not standardized across product revisions and can be misleading (red may be GND, not power). Always confirm the pin assignment with a multimeter — measure DC voltage between the two outer wires to locate pin 4 (+5V) — before connecting anything to the STM32 or level shifter. The [VE.Direct Protocol Specification](https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.34.pdf) is the authoritative reference for your specific device revision.
-
-
-
-The standard Victron VE.Direct cable (ASS030530200 / ASS030530300) terminates in bare wires on the host end. Connect pin 3 (TX) and pin 1 (GND); leave pins 2 and 4 unconnected or tape them off.
-
-### Pin-by-pin wiring
-
-**SmartShunt (Battery shunt / Serial1):**
-- **SmartShunt VE.Direct pin 3 (TX)** → 10 kΩ resistor → **CX header RX pin**, with the junction also connected to → 20 kΩ resistor → **GND**. The midpoint of the two resistors is the MCU RX input. This drops 5 V to ~3.33 V.
-- **SmartShunt VE.Direct pin 1 (GND)** → **CX header GND**.
-- Pins 2 and 4 are not connected — the protocol is broadcast-only; the host never writes to the device, and the host is externally powered.
-
-**Victron temperature sensor (SmartShunt temp-sensor port):** Plug the optional Victron Temperature Sensor for SmartShunt directly into the SmartShunt's dedicated temperature-sensor port (a small JST connector on the SmartShunt body, separate from the VE.Direct port). No wiring to the Notecarrier CX is needed; the SmartShunt reads the sensor internally and broadcasts the temperature value on the VE.Direct wire, where the firmware picks it up automatically. This sensor is required for `bat_temp_c` to appear with a real value in summary Notes and for the `temp_high` alert to evaluate.
-
-**SmartSolar MPPT (Solar charger / SoftwareSerial on D9):**
-- **MPPT VE.Direct pin 3 (TX)** → 10 kΩ resistor → **CX header D9**, with the junction also connected to → 20 kΩ resistor → **GND**.
-- **MPPT VE.Direct pin 1 (GND)** → **CX header GND**.
-- **CX header D10** is defined as the SoftwareSerial TX pin in firmware but is not connected to anything — leave it NC.
-
-**Power — complete path from battery bus to Notecarrier CX:**
-
-The Traco TSR 1-2450 (12 V and 24 V sites) or TSR 1-4850 (48 V telecom sites) steps the battery bus voltage down to a regulated 5 V rail. The TSR 1 is a non-isolated SIP-3 switcher: **Pin 1 = VIN+, Pin 2 = GND (shared input/output return), Pin 3 = VOUT+**. Verify pinout against the [Traco TSR 1 datasheet](https://www.tracopower.com/int/wp-content/uploads/sites/2/documents/tsr1.pdf) before soldering — do not rely on silkscreen alone.
-
-Wire the complete power chain in this order:
-
-1. **Battery bus positive tap → inline fuse → Traco VIN+ (Pin 1).** Install a 2 A glass-cartridge or automotive blade fuse at the battery bus tap point, as close to the battery or busbar as practical. The TSR 1 is rated 1 A output; a 2 A fuse protects the converter and the 22–24 AWG wiring used at this current level while providing headroom for inrush. A fuse holder with a visual indicator (e.g. Littelfuse OMNI-BLOK series or equivalent) simplifies field commissioning.
-2. **Battery bus return (negative) → Traco GND/VIN− (Pin 2).** Connect the battery-bus return directly to the converter's GND pin. Do not float the input return.
-3. **Traco VOUT+ (Pin 3) → Mojo BAT pin** (when Mojo is installed for bench validation. See §9). Route the 5 V output through the Mojo so it measures all current drawn from the regulated rail. When Mojo is not installed, connect VOUT+ directly to step 4.
-4. **Mojo LOAD pin → Notecarrier CX +VBAT pin.** The Mojo passes current through and measures cumulative mAh.
-5. **Traco GND (Pin 2) / system return → Notecarrier CX GND pin.** The TSR 1's shared GND is both the input return and the output return; connect it to the Notecarrier CX GND. This is also the signal ground for the VE.Direct cable shields — connect the battery-bus return and signal ground together at one point only (at the converter or at the shunt negative terminal, not both) to avoid a ground loop through the VE.Direct cables.
-
-
-
-**Polarity verification before first power-on.** With VIN connected but the VOUT wire to the Notecarrier CX left disconnected, apply power and measure VOUT+ (Pin 3) to GND (Pin 2). You should read 5.0 V ± 2%. If the voltage is absent or reversed, recheck the VIN polarity at Pin 1 and Pin 2 before connecting the Notecarrier CX. Off-grid battery banks can deliver thousands of amps into a reverse-polarity fault; verify before connecting.
-
-
-
-**Bench power (USB):** During bench bring-up without the DC-DC converter, power the Notecarrier CX from USB via the +VUSB pin or USB-C port. Do not connect +VBAT and +VUSB simultaneously.
-
-**Cellular antenna (NOTE-MBGLW path):**
-
-The Notecarrier CX routes the NOTE-MBGLW's `MAIN` u.FL port to a u.FL pigtail lead on the carrier board. An external u.FL cellular/LTE antenna must be connected to this lead; the Notecarrier CX has no onboard PCB trace cellular antenna. See the [Blues antenna guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/) for certified antenna options.
-
-- **Non-metallic (plastic or ABS) enclosure:** Connect a compact flexible u.FL LTE antenna and position it inside the enclosure away from metal components, or route the cable lead through a small cable gland and mount the antenna on the enclosure exterior.
-- **Metallic enclosure (steel electrical cabinet, aluminum panel):** Connect a u.FL-to-SMA pigtail to the `MAIN` u.FL pigtail lead; thread an IP67-rated SMA bulkhead connector through the enclosure wall; mount the cellular antenna on the external SMA port. This routes the RF path outside the metallic shell that would otherwise shield it significantly.
-
-The NOTE-MBGLW includes a GNSS receiver, but GNSS is not used in this project; the design operates in cellular mode only and no GPS/GNSS antenna is required. If site geolocation is needed in a future enhancement, the Notecard's onboard GNSS can be enabled without firmware changes.
-
-**Satellite antenna feedthrough (NOTE-NBGLWX only):**
-- Notecard for Skylo has two u.FL connectors: `MAIN` for the Skylo-certified satellite/LTE antenna (included in the kit) and `GPS` for the passive GNSS antenna (Quectel YCA001BA). Both antennas must be mounted **outside the enclosure** with an unobstructed view of the sky; satellite signals cannot penetrate a metal enclosure wall.
-- **MAIN antenna:** Connect the included Skylo antenna to the `MAIN` u.FL port via the SparkFun WRL-09145 (or equivalent RG316/RG178) pigtail. The pigtail's SMA female end connects to an IP67-rated SMA bulkhead connector threaded through the enclosure wall; the antenna mounts on the bulkhead's external SMA port. The bulkhead provides its own weather seal.
-- **GPS antenna (Quectel YCA001BA):** Plug the antenna's u.FL connector into the `GPS` u.FL port on the Notecard. Route the flexible cable lead through an M16 IP67 cable gland in the enclosure wall and mount the flexible patch flat on the exterior of the enclosure.
-- Maintain gentle cable curves throughout; avoid sharp bends that kink the coax feed lines. Keep runs as short as practical to minimize insertion loss, especially on the satellite band.
-- Use only the Skylo-certified antenna supplied with Notecard for Skylo kit on the MAIN port. Substituting a different antenna voids Skylo network certification and may result in the device being blocked from the network. See the [Blues antenna guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/) for full certification and placement requirements.
-
-
-
-VE.Direct cables are short (0.9 m or 1.8 m). Mount the Notecarrier CX close to the SmartShunt and MPPT to avoid signal integrity issues on long runs.
-
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid); it looks like `com.your-company.your-name:solar-battery-site`.
-2. **Set the ProductUID in firmware.** Open `firmware/solar_battery_controller/solar_battery_controller.ino` and replace the empty string on the `#define PRODUCT_UID ""` line with your project's value.
-3. **Claim the Notecard.** Power the assembled unit. On its first cellular connection the Notecard associates itself with your Notehub project automatically. The device appears in the **Devices** tab within a minute or two.
-4. **Create a Fleet per site type.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) (and [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules)) group devices for shared configuration. A natural split: one fleet for 12V cabin installations, another for 24V telecom sites. Each fleet can carry different alert thresholds appropriate to its battery chemistry and load profile.
-5. **Set environment variables.** In Notehub: navigate to **Fleet → Environment** (or **Device → Environment** for a per-unit threshold override). The device pulls them on its next inbound sync — no firmware reflash, no site visit required.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `soc_alert_pct` | `20.0` | State-of-charge (%) threshold below which `soc_low` alert fires. Typical range: 15–25% depending on battery chemistry. |
- | `bat_temp_max_c` | `45.0` | Battery temperature (°C) threshold above which `temp_high` fires. Only evaluated if a Victron Temperature Sensor is plugged into the SmartShunt temp port. Typical for lead-acid: 50–55 °C; for lithium: 40–45 °C. |
- | `load_alert_w` | `1000.0` | Load-draw (W) threshold above which `load_high` fires. Size this to your site's rated load budget. Example: a tower with 600 W peak load should set this to 650–700 W to allow headroom; a data-center-grade 1.5 kW load should set this to 1600–1700 W. |
- | `harvest_deficit_days` | `0.0` | Consecutive days without the MPPT reaching a full-charge state (Float, Absorption, Equalize, or Auto Equalize) before `harvest_deficit` fires. `0.0` disables the check. Recommended: `2.0` for well-sized systems, `3.0–4.0` for systems with seasonal or multi-day cloud patterns. Only evaluated when MPPT data is present in the window. |
- | `sample_interval_sec` | `900` | Seconds between host wakes (minimum 60, maximum 3600). Shorter intervals consume more power per unit time but improve resolution. Default 900 seconds = 15 minutes; typical range 300–1800 s. |
- | `report_interval_min` | `240` | Minutes between summary Notes (minimum 15, maximum 1440). When this changes, firmware immediately resets the window and re-issues `hub.set` to sync Notecard's outbound cadence on the same cycle. For Skylo NTN deployments, set to `1440` (once daily) to reduce satellite data consumption. |
- | `sync_outbound_min` | *(auto: equals `report_interval_min`)* | Notecard outbound sync interval in minutes (15–1440). Leave unset to track `report_interval_min` automatically, or set explicitly to decouple (e.g., sync every 2 hours but summarize every 4). For Skylo: set `report_interval_min=1440` and leave this unset. |
- | `sync_inbound_min` | *(auto: 2 × `report_interval_min`)* | Notecard inbound sync interval in minutes (30–2880) for pulling Fleet variable updates. Defaults to 2× `report_interval_min`. Reduce to deliver threshold changes faster (costs additional satellite or cellular data). |
-
-6. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `solar_alert.qo` (to an on-call or NOC platform; these are time-sensitive) and a second for `solar_summary.qo` (to a long-term analytics or SCADA store; four-hour batches are fine). Separating the two Notefiles at the source means each can target a different downstream system at a different urgency without any server-side filtering.
-
-### Skylo (NOTE-NBGLWX) deployment guidance
-
-The NOTE-NBGLWX manages radio-mode selection internally; the firmware is identical to the NOTE-MBGLW path. The satellite transport, however, has four characteristics that differ materially from cellular and must inform how you configure and commission the device.
-
-**Data budget.** The NOTE-NBGLWX includes 10 KB of Skylo satellite data. The templated `solar_summary.qo` Note encodes to roughly 80–120 bytes on the satellite wire — well within the 256-byte Skylo payload ceiling. Each untemplated `solar_alert.qo` Note is similarly compact. Where the allocation is consumed fastest is *inbound* syncs: each satellite inbound ping costs approximately 50 bytes even when no environment variable changes are pending. At the default 4-hour summary cadence (6 outbound sessions per day, 3 inbound sessions per day at `inbound = outbound × 2 = 8 h`), a site operating exclusively on satellite would exhaust the included 10 KB in roughly 11 days. **For satellite-primary deployments, set `report_interval_min` to `1440` (once per day) in the Fleet Environment before commissioning.** Daily summaries consume approximately 4–5 KB per month at normal alert rates, keeping well within the included allocation. At-threshold alerts (`soc_low`, `temp_high`, `load_high`, `harvest_deficit`) use `sync:true` and fire immediately regardless of the summary cadence — they remain the most valuable transmissions and are few in number. To further reduce inbound cost, trigger manual inbound syncs from the Notehub device page only when you need to push threshold changes rather than relying on the scheduled inbound interval. See the [satellite best practices guide](https://dev.blues.io/starnote/satellite-best-practices/) for full data-budget guidance.
-
-**Alert latency.** On LTE Cat-1 bis, `sync:true` alerts reach Notehub in seconds. On the satellite path the Notecard must acquire a GPS fix (fast when location is fixed. See below), wait for a satellite visibility window, and complete the NTN session. **Expect end-to-end alert latency of 2–10 minutes on satellite.** This is well-suited to the "site will go dark in the next few hours" scenario this project targets; size your NOC response SLAs accordingly rather than expecting cellular-equivalent latency.
-
-**Satellite session power.** A satellite session is longer and more power-intensive than a cellular session. The Notecard must power the GPS receiver, search for a satellite visibility window, and drive the NTN modem through a transmission — a process that can run 2–5 minutes of elevated radio-active current, compared to the 15–30 seconds typical of LTE Cat-1 bis. With a daily outbound cadence the number of radio sessions drops from 6 (LTE Cat-1 bis default) to 1 per day, which largely offsets the per-session cost increase. Validate the energy profile with Mojo (§8) at the bench using the satellite cadence before field deployment — the NOTE-NBGLWX power table and expected Mojo trace in §8 describe what to look for. See the [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for SKU-specific power curves.
-
-**Fixed GPS location.** The NOTE-NBGLWX must know its location to find overhead satellites. For any stationary installation, lock the position once at commissioning using the blues.dev In-Browser Terminal or a bench serial session:
-```json
-{"req": "card.location.mode", "mode": "fixed", "lat": 37.123, "lon": -121.456}
-```
-Replace `lat`/`lon` with the site's actual GPS-derived coordinates. **Do not use `tower_lat`/`tower_lon` from `_session.qo` for this** — those fields are cell-tower–derived estimates, not GPS fixes, and can be off by kilometers. To obtain accurate coordinates, ensure the GPS antenna is connected and has a clear sky view, then issue `{"req": "card.location"}` from the same terminal; the response `lat` and `lon` fields carry the current GPS fix. Use those values in the `card.location.mode` fixed command above. A fixed location eliminates repeated GPS cold-starts before each satellite session and materially reduces both session startup time and power consumption.
-
-**Commissioning order.** If there is any terrestrial cellular or WiFi coverage at or near the deployment site, power the unit there first. The Notecard registers with Notehub over LTE Cat-1 bis, confirms all Note templates, and acquires an accurate time reference — steps that accelerate the first satellite session at the final remote location. If the site is beyond all cellular coverage, allow 5–15 minutes on first power-up for the Notecard to acquire a GPS fix and complete the initial satellite registration.
-
-**First-boot environment variables.** On first boot the firmware calls `hub.set` to associate the Notecard with your Notehub project, then calls `env.get` to pull any pre-configured Fleet environment variables. However, the Notecard must complete an **inbound sync with Notehub** before `env.get` returns Fleet-level values — a cold-start device that has never synced has no cached env vars yet. On LTE Cat-1 bis the first inbound sync typically completes within one to two minutes of power-on, so the second wake onward reflects pre-configured Fleet variables. The exposure window is at most one 15-minute sample interval before the correct env vars take effect.
-
-For NOTE-NBGLWX Skylo deployments this window matters more: a single wake at the 4-hour cellular default can consume a meaningful fraction of the 10 KB satellite data budget. The safest approach is to compile with `-DSKYLO_BUILD` in your build flags (see §7.1 and the `#ifdef SKYLO_BUILD` block in `firmware/solar_battery_controller/solar_battery_controller_notecard_helpers.h`). This changes the compile-time default for `DEFAULT_REPORT_INTERVAL_MIN` to 1440 minutes, so even the very first wake — before any Notehub inbound sync — uses the satellite-safe daily cadence. Fleet environment variables then tune thresholds and cadence further after the first inbound sync arrives. If you are not using the `SKYLO_BUILD` flag, set `report_interval_min = 1440` in your Fleet Environment before commissioning and allow a minute or two after first power-on for the first inbound sync to deliver it.
-
-### What you should see in Notehub
-
-Within a minute of first power-on, the Events tab in your project should begin populating:
-
-- **`_session.qo`** — automatic Notecard housekeeping on each cellular session. Confirms the radio is reaching Notehub. If this never appears, check the antenna connection and `PRODUCT_UID`.
-- **`solar_summary.qo`** — one per `report_interval_min` (default every 4 hours). Every template field is always present. A typical healthy-site body during daytime with solar contributing but load exceeding PV output — the MPPT is in Float state (`cs: 5`, battery at working SoC) while the battery supplements solar to cover the full site load:
- ```json
- {
- "bat_v": 26.1,
- "bat_a": -3.2,
- "bat_w": -83.5,
- "soc_pct": 78.4,
- "bat_temp_c": 22.1,
- "pv_v": 34.8,
- "pv_w": 142.0,
- "load_w": 225.5,
- "yield_kwh": 1.24,
- "ttg_min": -1,
- "cs": 5
- }
- ```
- `bat_a` negative means the battery is net discharging (load exceeds solar input). In this example solar is producing 142 W, load is drawing 225.5 W, and the battery is supplementing the difference (~83.5 W). `cs` of 5 = Float (the MPPT charge algorithm has reached its float stage; the battery is at working SoC and solar is meeting its maintenance demand, but site loads still exceed current PV output). `ttg_min` of −1 means the SmartShunt is present but the battery is not in an active discharge (TTG is inapplicable or infinite); a positive value such as `"ttg_min": 312` means 5 h 12 minutes of estimated run time remaining. Fields show −9999 (float sentinel) or −1 (cs sentinel) rather than being absent when no valid reading was available for that metric in the window, for example, `"bat_temp_c": -9999.0` means no temperature sensor is connected to the SmartShunt.
-- **`solar_alert.qo`** — emitted only on a threshold trip, with `sync:true` for immediate delivery. Four alert types are defined; context values vary by alert. Example low-SoC alert:
- ```json
- {
- "alert": "soc_low",
- "v1": 18.2,
- "v2": 24.1,
- "v3": -156.8
- }
- ```
- Fields: `v1` = current SoC %, `v2` = battery voltage V, `v3` = net battery power W (negative = discharging). See [§8](#8-data-flow) for the complete alert taxonomy and context value meanings for each alert type (`temp_high`, `load_high`, `harvest_deficit`).
-
-## 7. Firmware Design
-
-Five files:
-- [`firmware/solar_battery_controller/solar_battery_controller.ino`](firmware/solar_battery_controller/solar_battery_controller.ino) — main application sketch (orchestration, accumulation, alert evaluation)
-- [`firmware/solar_battery_controller/solar_battery_controller_helpers.h`](firmware/solar_battery_controller/solar_battery_controller_helpers.h) — VE.Direct parser header
-- [`firmware/solar_battery_controller/solar_battery_controller_helpers.cpp`](firmware/solar_battery_controller/solar_battery_controller_helpers.cpp) — VE.Direct parser implementation
-- [`firmware/solar_battery_controller/solar_battery_controller_notecard_helpers.h`](firmware/solar_battery_controller/solar_battery_controller_notecard_helpers.h) — `PersistState` struct, all configuration constants, and Notecard I/O declarations
-- [`firmware/solar_battery_controller/solar_battery_controller_notecard_helpers.cpp`](firmware/solar_battery_controller/solar_battery_controller_notecard_helpers.cpp) — Notecard I/O implementation (`hub.set`, templates, env vars, Note queuing)
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Install via Arduino Boards Manager (search "STM32 MCU based boards") or add the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**. Select **Blues Cygnet** as the board target (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-- **`Blues Wireless Notecard`** library — [`note-arduino`](https://github.com/blues/note-arduino). Install via the Arduino Library Manager (`arduino-cli lib install "Blues Wireless Notecard"`). Check the [note-arduino releases](https://github.com/blues/note-arduino/releases) for the latest stable version before starting a new project.
-
-**Flashing — Arduino IDE:** open `solar_battery_controller.ino`, select **STMicroelectronics → Blues Cygnet** as the board, and click **Upload**. The Notecarrier CX's onboard ST-Link debug interface enumerates as a virtual COM port on the same USB cable — no external programmer needed.
-
-**Flashing — `arduino-cli`:**
-```bash
-# Confirm the FQBN for your installed core version
-arduino-cli board listall | grep -i cygnet
-
-# Compile and upload (replace the FQBN and port to match your system)
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/solar_battery_controller/
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/solar_battery_controller/
-```
-
-**NOTE-NBGLWX Skylo build flag:** To compile with a satellite-safe default cadence (1440-minute daily summary from the very first wake), add `-DSKYLO_BUILD` to the compiler flags. In the Arduino IDE this goes in **File → Preferences → Additional build options**; with `arduino-cli` pass `--build-property "compiler.cpp.extra_flags=-DSKYLO_BUILD"`. See §5 Skylo deployment guidance for context.
-
-```bash
-# Example: Skylo build
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- --build-property "compiler.cpp.extra_flags=-DSKYLO_BUILD" \
- firmware/solar_battery_controller/
-```
-
-After upload, open the serial monitor at **115200 baud**. On first boot you'll see the Notecard configuration log; after that, each 15-minute wake prints a one-line `[summary]` or `[alert]` entry, then goes quiet as the host re-sleeps.
-
-### 7.2 Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard configuration (`hub.set`, templates, accelerometer disable) | `notecardFirstBoot`, `applyHubSetIfChanged`, `defineTemplates` |
-| Environment variable fetch and threshold refresh | `fetchEnvOverrides` |
-| VE.Direct frame reading (two devices) | `readVEDirectFrame` in helpers |
-| Sample accumulation into rolling window averages | `accumulate` |
-| Alert evaluation and immediate-sync Notes | `checkAndSendAlerts`, `checkHarvestDeficit`, `sendAlert` |
-| Summary Note construction and emission | `sendSummary` |
-| Persistent state serialization across sleep cycles | `PersistState` struct + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### 7.3 VE.Direct reading strategy
-
-**VE.Direct protocol primer.** Victron's VE.Direct is a one-wire async text protocol at 19200 baud 8N1. The device broadcasts one frame per second — no polling, no handshake. Each frame is a series of `LABELVALUE` lines terminated by a `Checksum` line. The checksum byte makes the sum of all bytes in the frame (including the `Checksum` line itself) equal zero modulo 256. On a healthy bus you get 3–4 complete frames in a 3-second window.
-
-`readVEDirectFrame()` in the helpers file reads characters from the given `Stream` object until it sees a complete, checksum-verified frame or the timeout expires (default 3 seconds). It extracts only the fields relevant to the SmartShunt or MPPT; all other labels are silently ignored. The Notecarrier CX's UART (Serial1) is used for the SmartShunt; a `SoftwareSerial` instance on D9 handles the MPPT. Both are read sequentially at wakeup.
-
-**Why not poll both simultaneously?** VE.Direct is a unidirectional broadcast. Both devices are always transmitting; we just listen to one at a time. Reading them sequentially adds ~6 seconds of active time per wake cycle, which is negligible against a 15-minute sleep interval.
-
-### 7.4 Event payload design
-
-`solar_summary.qo` uses a [Note template](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) registered at first boot. Templated Notes are stored as fixed-length records on the Notecard rather than free-form JSON, reducing wire size by roughly 3–5× — meaningful on a system that will run for years on a prepaid cellular SIM. Every field in the template schema is **always present** in every Note body, using explicit sentinel values when no valid samples were collected for that metric in the window:
-
-- `SUMMARY_SENTINEL_F = −9999.0` — all float fields (`bat_v`, `bat_a`, `bat_w`, `soc_pct`, `bat_temp_c`, `pv_v`, `pv_w`, `yield_kwh`, `load_w`).
-- `SUMMARY_SENTINEL_TTG = −9999` — `ttg_min` when no SmartShunt data was received in the window. A value of −1 (separate from the sentinel) means the SmartShunt was present but the battery was not in an active discharge (TTG is inapplicable or infinite). A value ≥ 0 is the active discharge estimate in minutes.
-- `SUMMARY_SENTINEL_CS = −1` — `cs` when no MPPT data was received. All valid VE.Direct CS codes for the SmartSolar MPPT are non-negative (0, 2–5, 7, 245, 247, 252. See `VED_CS_*` constants in `solar_battery_controller_helpers.h`), so −1 unambiguously signals no MPPT data without colliding with any real charge-state value.
-
-This fixed-schema approach follows established Blues reference design practice: the template always fires fully populated, downstream analytics can rely on column presence and never see a field collapse to the Notecard's default zero for a missing column, and sensor faults are distinguishable from real zero readings. `ttg_min` is reset to −1 at the start of each summary window (`resetAccumulators` sets `state.ttg_min = -1`) to prevent a stale value from a prior window leaking forward. If an entire window produces no valid samples from either device, `sendSummary` skips the Note entirely and opens a new window; no all-sentinel record is emitted.
-
-`solar_alert.qo` is **not** templated: alert types have different supporting fields and the volume is low enough that free-form JSON is fine. The `alert` field names the failure condition; `v1`, `v2`, `v3` carry context values whose meaning depends on the alert type:
-
-| Alert | `v1` | `v2` | `v3` |
-|---|---|---|---|
-| `soc_low` | current SoC % | battery voltage V | net battery power W |
-| `temp_high` | battery temp °C | battery voltage V | SoC % |
-| `load_high` | load draw W | SoC % | PV power W |
-| `harvest_deficit` | avg SoC % over window | consecutive windows without Float/Absorb | today's yield kWh |
-
-### 7.5 Low-power strategy
-
-The site is battery-powered; power budget matters. The host MCU spends almost all of its time completely off. After each wake cycle, `NotePayloadSaveAndSleep` serializes the `PersistState` struct to Notecard flash and issues a [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) sleep request that cuts power to the host entirely. When the ATTN timer fires 15 minutes later (default), the Notecarrier CX re-applies power, the Cygnet re-enters `setup()` from cold, and `NotePayloadRetrieveAfterSleep` restores the accumulated state. From the firmware's perspective, the sleep call looks like a single function; the Notecard does the rest.
-
-The Notecard's own idle draw is ~8–18 µA between cellular sessions (per the Blues [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)). The +VBAT rail draws more than the Notecard alone: each voltage-divider network (10 kΩ + 20 kΩ = 30 kΩ total) pulls ~167 µA continuously while the VE.Direct device is powered, and two dividers together add ~334 µA. Mojo at the +VBAT rail therefore sees approximately **350–380 µA** between wakes (dividers ~334 µA, plus Notecard and board quiescent ~16–44 µA). The Traco TSR 1 DC-DC converter draws approximately 1–2 mA at its VIN input from the battery bus — this is an input-side draw, upstream of the 5 V output rail, and is not visible to Mojo at the +VBAT pin. Measuring the total battery-bus draw including converter quiescent requires a separate instrument (e.g. a clamp meter or series shunt) on the converter's VIN supply wire. See §8 for the complete system current table. The Notecard's `outbound` sync cadence (default 4 hours) controls when it wakes the radio — at 15-minute sample intervals and a 4-hour transmit cadence, the radio fires 6 times per day rather than 96.
-
-Sampling and transmission cadences are deliberately decoupled: the device samples every 15 minutes but only opens a cellular session every 4 hours. Alert Notes bypass this by setting `sync:true`, which tells the Notecard to open a session immediately when an alert is queued; it won't wait for the next 4-hour window.
-
-### 7.6 Retry and error handling
-
-- The first Notecard transaction in `notecardFirstBoot` uses `notecard.sendRequestWithRetry(req, 5)` to survive the cold-boot I²C readiness race documented in the note-arduino library. The initial `env.get` inside `fetchEnvOverrides` is wrapped in its own 5-attempt retry loop for the same reason — a restored (non-first) boot arrives at `fetchEnvOverrides` before any prior retry has had a chance to confirm the Notecard is ready.
-- `readVEDirectFrame` enforces a 3-second per-device timeout. If a device doesn't respond (powered off, disconnected, connector fault), the function returns `false` and the corresponding fields are skipped in `accumulate` — metrics derived only from the missing device will carry `0` valid samples for that window, and `sendSummary` will emit the sentinel value (−9999 for floats, −1 for `cs`) for those fields. A complete loss of SmartShunt data for an entire window is visible as −9999 on `bat_v`, `soc_pct`, etc. in `solar_summary.qo`, making the sensor fault obvious in Notehub.
-- `#define ALERT_COOLDOWN_SAMPLES 2` (~30 minutes at the default 15-min interval) controls how often a persistent fault re-alerts. Each alert fires immediately on the first trip, then repeats every 2 wakes (~30 minutes) while the condition holds. When the condition clears, the active flag and cooldown counter both reset so the next trip fires immediately again. One alert every ~30 minutes per condition is the right balance for on-call notification without saturating satellite data quota.
-- Summary send failures do not open a new window. When `note.add` for `solar_summary.qo` fails, `samples_until_summary` is left at `0` (not reset). On the next wake the firmware retries the send *before* reading new VE.Direct data, so the closed window's averages are never mixed with fresh readings. `resetAccumulators()` and the window-open only happen after a confirmed successful queue.
-- Environment variable clamp logic in `fetchEnvOverrides` rejects `sample_interval_sec` values outside 60–3600 and `report_interval_min` values outside 15–1440, preventing operator typos from making the device unreachable.
-
-### 7.7 Key code snippet 1: Note template registration
-
-Templates turn each summary Note into a fixed-length on-wire record. The placeholder values encode the wire type: `14.1` = 4-byte IEEE-754 float; `14` = 4-byte signed integer; `12` = 2-byte signed integer.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "solar_summary.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "bat_v", 14.1);
-JAddNumberToObject(body, "soc_pct", 14.1);
-JAddNumberToObject(body, "pv_w", 14.1);
-JAddNumberToObject(body, "yield_kwh", 14.1);
-JAddNumberToObject(body, "load_w", 14.1);
-JAddNumberToObject(body, "ttg_min", 14);
-JAddNumberToObject(body, "cs", 12);
-notecard.sendRequest(req);
-```
-
-### 7.8 Key code snippet 2: immediate-sync alert
-
-`sync:true` tells the Notecard to bypass the next `outbound` window and open a cellular session immediately.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "solar_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "soc_low");
-JAddNumberToObject(body, "v1", shunt.soc_pct);
-JAddNumberToObject(body, "v2", shunt.bat_v);
-JAddNumberToObject(body, "v3", shunt.bat_w);
-notecard.sendRequest(req);
-```
-
-### 7.9 Key code snippet 3: sleep between samples
-
-`NotePayloadSaveAndSleep` writes the state struct to Notecard flash and arms the ATTN timer to cut host power for `sample_interval_sec` seconds. The next call to `NotePayloadRetrieveAfterSleep` in the subsequent `setup()` rehydrates it.
-
-```cpp
-NotePayloadDesc desc = {0, 0, 0};
-NotePayloadAddSegment(&desc, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&desc, state.sample_interval_sec, NULL);
-// Code below here only runs if ATTN is not controlling host power (bench test):
-delay(state.sample_interval_sec * 1000UL);
-```
-
-### 7.10 Key code snippet 4: VE.Direct load computation
-
-Load draw is derived algebraically rather than measured directly:
-
-```cpp
-// load_w = solar_in − battery_net
-// bat_w is positive when charging (solar exceeding load)
-// bat_w is negative when discharging (load exceeding solar)
-// Examples:
-// pv_w=300, bat_w=+100 → load = 200 W (solar charging battery + powering load)
-// pv_w=100, bat_w=-150 → load = 250 W (solar + battery both powering load)
-float load = mppt.pv_w - shunt.bat_w;
-```
-
-## 8. Data Flow
-
-
-
-Every 15 minutes (default `sample_interval_sec = 900`) the firmware wakes, reads both VE.Direct devices, and accumulates one data point into the current summary window:
-
-- **Collected per sample.** Battery voltage (V), battery current (A, signed), net battery power (W, signed), SoC (%), battery temperature (°C, when sensor present), panel voltage (V), panel power (W), computed load (W). Latest-only: daily yield (kWh), charge state (integer enum), time-to-go (minutes).
-- **Transmitted.**
- - `solar_summary.qo` — once per `report_interval_min` (default every 4 hours = 16 samples per window). Every template field is always present. Each averaged field carries the mean of its valid samples, or −9999 when no valid samples exist for that metric. `yield_kwh` and `cs` carry the latest value at window-close, not an average; `cs` is −1 when no MPPT data was received. `ttg_min` is −9999 when no SmartShunt data was received, −1 when the shunt was present but the battery was not discharging, or the active minutes-remaining estimate. If an entire window has no valid samples from either device, the Note is skipped entirely.
- - `solar_alert.qo` — emitted immediately on a threshold trip, `sync:true`. Deduplicated by a ~30-minute per-alert cooldown.
-- **Routed.** Notehub forwards `solar_alert.qo` to real-time notification destinations (NOC dashboards, on-call paging, SMS gateway) and `solar_summary.qo` to analytics or long-term storage. Separate Notefiles means separate routes, separate urgency tiers, separate retention policies.
-- **Alert triggers.**
- - `soc_low` — SoC below `soc_alert_pct`. Fires once per ~30 minutes while SoC remains low. Context: current SoC, battery voltage, net battery power.
- - `temp_high` — battery temperature above `bat_temp_max_c` (only when a temperature sensor is wired to the SmartShunt). Context: current temp, battery voltage, SoC.
- - `load_high` — computed load draw above `load_alert_w`. Fires once per ~30 minutes. Context: load W, SoC, current PV power.
- - `harvest_deficit` — MPPT has not reached a full-charge state (Float `cs=5`, Absorption `cs=4`, Equalize `cs=7`, or Auto Equalize `cs=247`) for `harvest_deficit_days` consecutive days (disabled by default; enable via env var). Fires once per ~24 hours while the deficit persists. Context: average SoC % over the window, number of consecutive windows without a full-charge state, today's accumulated solar yield kWh. This alert is the primary indicator of a recharge imbalance — a degraded array, persistent cloud cover, or load growth that the system can no longer recover from overnight. The firmware does not count Equalize/Recondition as a deficit: these cycles run after the battery has already reached Float, so their presence confirms a healthy charge cycle completed.
-
-## 9. Validation and Testing
-
-**Expected steady-state behavior.** A healthy site running with solar in bulk or float mode produces one `solar_summary.qo` every 4 hours and zero `solar_alert.qo` events. `bat_a` will be slightly positive (battery absorbing a maintenance trickle) during the day and slightly negative at night (load drawing from battery overnight). `soc_pct` should be above 80% on a well-sized system by mid-morning.
-
-**Using Mojo to validate the power profile.** The Notecard's published standalone idle draw is ~8–18 µA @ 5V. See the [low-power firmware design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for the authoritative figures by SKU. The assembled monitor draws more at its +VBAT input because the voltage dividers and DC-DC converter add quiescent current even when the host is off. The table below breaks out both figures so the Mojo trace makes sense:
-
-#### NOTE-MBGLW path (LTE Cat-1 bis cellular)
-
-| Phase | Notecard-only | Measured by Mojo at +VBAT |
-|---|---|---|
-| Between wakes (host off, ATTN sleeping) | ~8–18 µA | **~350–380 µA** (dividers ~334 µA + Notecard ~8–18 µA + board quiescent) |
-| Host active (VE.Direct reads + I²C) | — | ~6–8 mA for ~6–8 seconds per wake |
-| Notecard cellular session (LTE Cat-1 bis outbound) | ~250 mA avg, ≤2 A peak, for ~15–30 seconds | same — radio dominates |
-
-> **Mojo measurement boundary.** Mojo sits between the Traco 5 V output and the Notecarrier CX +VBAT pin, so it measures current on the 5 V output rail only. The Traco TSR 1's no-load input quiescent (~1–2 mA) is drawn from the battery bus at the converter's VIN pin — upstream of the 5 V rail, and is not visible to Mojo. To measure total battery-bus draw including converter quiescent, place a separate shunt or clamp meter on the converter's VIN supply wire.
-
-The **~350–380 µA Mojo idle baseline** at +VBAT comes from the two always-energized 10 kΩ/20 kΩ divider networks (~5 V ÷ 30 kΩ = 167 µA each, × 2 = ~334 µA) plus Notecard and board quiescent (~16–44 µA). The dividers are the primary target if the +VBAT baseline needs to be reduced; switching to a powered-off level-shifting stage would eliminate ~334 µA.
-
-A rough 24-hour Mojo estimate at default settings (6 cellular sessions/day, 96 host wakes/day): voltage-divider standing current dominates (~334 µA × 24 h ≈ **8 mAh**), six LTE Cat-1 bis radio sessions add roughly **8–10 mAh**, and 96 host wakes add roughly **1 mAh** — approximately **17–20 mAh per 24 hours** measured by Mojo at the +VBAT rail. (Additional battery-bus draw not measured by Mojo: converter input quiescent ~1–2 mA × 24 h ≈ 24–48 mAh at the battery bus voltage.) Verify once at bench bring-up with Mojo; a daily Mojo total more than 3× above ~20 mAh usually indicates either the host is not sleeping (continuous multi-mA elevation above the ~380 µA floor) or the radio is struggling with marginal signal (correctly-spaced sessions each running 60+ seconds).
-
-To use Mojo: splice it inline between the DC-DC converter 5 V output and the Notecarrier CX +VBAT pin, connect its Qwiic output to the Notecarrier CX I²C header, and leave the unit running for 24 hours. A healthy LTE Cat-1 bis trace shows a **flat ~350–380 µA baseline** (the +VBAT system idle floor), brief blips to ~6–8 mA every 15 minutes (host wake), and one 15–30-second burst to ~250 mA every 4 hours (cellular session). Any continuous elevation above the ~380 µA floor that doesn't correlate with a host wake or radio burst indicates an unexpected active current path.
-
-#### NOTE-NBGLWX path (Skylo NTN satellite + LTE-M)
-
-The satellite path has a fundamentally different power profile from cellular. A satellite session requires the GNSS receiver to confirm the device's position, then drives the NTN modem through an uplink/downlink sequence that typically runs 2–5 minutes, versus the 15–30 seconds of an LTE Cat-1 bis session. The per-session energy is higher and the session is longer, but the recommended satellite cadence is once per day versus the six daily sessions on LTE Cat-1 bis. The six LTE Cat-1 bis sessions consume roughly 8 mAh/day combined; the single daily satellite session is estimated at ~3–12 mAh (see phase table for the per-phase breakdown). Either way, the whole-system daily budget is dominated by the continuous ~1–3 mA standing current floor — the same voltage dividers and DC-DC converter quiescent present on both paths — making the daily total **comparable to the LTE Cat-1 bis path** rather than substantially lower. The satellite path's standing-current-dominated total is shown in the breakdown below.
-
-| Phase | Measured by Mojo at +VBAT |
-|---|---|
-| Between sessions (host off, ATTN sleeping) | **~350–380 µA** (dividers ~334 µA + Notecard + board quiescent; converter input quiescent ~1–2 mA is at battery bus, not visible to Mojo, same boundary as LTE Cat-1 bis path) |
-| Host active (VE.Direct reads + I²C) | ~6–8 mA for ~6–8 seconds per wake (96 wakes/day) |
-| GPS acquisition — fixed location (warm fix) | ~30 mA for ~60 seconds ≈ 0.5 mAh |
-| NTN satellite session (transmit + receive) | ~250–500 mA peak during NTN data burst (~20–45 seconds); ~30–80 mA average over the remaining session overhead; 2–5 minutes total — estimated **~3–12 mAh/session** (validate with Mojo) |
-| LTE-M cellular fallback (when available) | similar profile to NOTE-MBGLW above |
-
-**Per-day Mojo energy estimate at the recommended daily satellite cadence** (1 NTN session/day, `report_interval_min = 1440`):
-- Voltage-divider standing (24 h × ~334 µA): **~8 mAh**
-- Notecard + board quiescent (24 h × ~18–44 µA): **~0.5 mAh**
-- 96 host wakes (~7 mA × ~7 seconds each): **~1.3 mAh**
-- GPS fix (warm, fixed location): **~0.5 mAh**
-- 1 NTN satellite session: **~3–12 mAh** (peak 250–500 mA burst ~20–45 seconds plus session overhead. See phase table)
-
-The voltage-divider standing current dominates — approximately **13–22 mAh/day** measured by Mojo at the +VBAT rail. Additional battery-bus draw not visible to Mojo includes the converter's input-side quiescent (~1–2 mA at the battery bus for 24 h). The passive dividers (~334 µA) are the primary lever for reducing the Mojo-measured baseline; the converter input quiescent is the primary lever for reducing total battery-bus draw. For reference, [Blues published data](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) shows the Starnote for Skylo at 60-minute NTN sync cadence consuming ~27 mAh over 12 hours (a full-system measurement that includes all idle time between sessions at that product's quiescent draw, not an isolated per-session energy figure); the NOTE-NBGLWX uses a similar NTN radio subsystem — treat this as an order-of-magnitude sanity check and validate the actual per-session energy with Mojo at the bench before field deployment. See the [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) for SKU-specific power curves.
-
-A healthy Skylo trace on Mojo shows: flat **~350–380 µA baseline** (the +VBAT system idle floor), brief blips to ~6–8 mA every 15 minutes (host wake), and once per day a multi-stage radio burst — a ~30 mA plateau for ~60 seconds (GPS fix), followed by a 2–5 minute NTN session that includes a peak ~250–500 mA transmit burst (~20–45 seconds) visible as a sharp spike within a lower-current session envelope (~30–80 mA for the rest of the session). If the GPS acquisition stage takes minutes rather than seconds, the device is likely not in fixed-location mode (see Skylo deployment guidance above).
-
-Mojo is a **bench bring-up tool**, not a production sensor. Once a firmware revision passes the trace check, deployed units don't need it.
-
-A Notecarrier CX and Notecard pair with two Victron VE.Direct devices to turn an opaque off-grid power system into a continuously-monitored, remotely-observable asset. The device wakes every 15 minutes, reads battery SoC, current, temperature, and solar harvest in a few seconds, and goes back to sleep — accumulating a window of averages that flush to Notehub every four hours. A site going dark gets a warning Note via cellular before the battery drops too far to communicate, giving the operations team a fighting chance to dispatch before the outage. None of this requires any site networking, IT coordination, or infrastructure that wasn't already there — just a 5V supply and two short VE.Direct cables.
-
-For the truly remote sites where terrestrial cellular coverage is marginal or absent, [Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) slots into the same M.2 carrier with no firmware changes, using the same API the Notecard Cell+WiFi uses. Deploying the Skylo variant requires mounting both antennas outdoors with an unobstructed sky view, as documented in §3 and §4. The pattern scales from a single off-grid cabin to a fleet of hundreds of remote towers, with per-fleet threshold management and per-device overrides handled entirely from Notehub.
-
-
-## 10. Troubleshooting and Common Issues
-
-| Symptom | Likely Cause | What to Check |
-|---------|--------------|---------------|
-| No `solar_summary.qo` or `_session.qo` Notes appear in Notehub after first power-on | ProductUID is unset, the antenna is disconnected, or the Notecard has no network coverage | Confirm the `PRODUCT_UID` macro in `solar_battery_controller.ino` matches the value shown on your Notehub project page. Verify the cellular antenna is connected to the `MAIN` u.FL pigtail (NOTE-MBGLW) or that both the satellite and GPS antennas are mounted outdoors with a clear sky view (NOTE-NBGLWX). Open the blues.dev In-Browser Terminal and issue `{"req":"hub.status"}` to see the current connection state. |
-| `solar_summary.qo` Notes arrive but every metric is `-9999` (the float sentinel) | VE.Direct frames are not parsing — wiring fault, wrong pin, or a checksum mismatch | Verify the resistor divider on each RX line (10 kohm in series, 20 kohm to GND) and confirm the divider midpoint reaches the correct CX header pin (RX for the SmartShunt, D9 for the MPPT). Use a multimeter to confirm pin 1 on the JST PH connector is GND and pin 3 is the device TX (wire colors are not standardized). Watch the serial monitor at 115200 baud for the per-cycle `[summary]` line — a parse failure prints a checksum or timeout warning. |
-| `soc_pct` reads implausibly high or low (for example, stuck at 100 percent or jumping by tens of percent between samples) | The SmartShunt's internal SoC calibration has not yet converged, or the shunt configuration does not match the installed battery bank | Use the Victron VictronConnect mobile app to confirm the SmartShunt's battery capacity (Ah), charged voltage, and Peukert exponent match the installed bank. Allow at least one full charge cycle for SoC to converge after the shunt is first installed. SoC is computed by the shunt itself, the firmware only reports the value broadcast on the wire. |
-| `pv_w` and `yield_kwh` show zero throughout the daytime hours | The MPPT has no PV input, the MPPT VE.Direct cable is not connected, or the SoftwareSerial RX pin is miswired | Verify the solar array breaker is closed and the MPPT is reporting nonzero panel voltage in VictronConnect. Confirm the MPPT VE.Direct cable lands on CX header D9 through its own divider, not on the SmartShunt UART (Serial1). A persistent `cs` value of `-1` in summary Notes confirms no MPPT frames were parsed in the window. |
-| Cellular session repeatedly fails (`_session.qo` shows non-zero `voltage` errors or `failure_reason` populated) | Marginal LTE Cat-1 bis signal at the install location, antenna placement inside a metallic enclosure, or insufficient peak current at the +VBAT rail | Check the `bars` and `rssi` fields in `_session.qo`. For metallic enclosures, route the antenna outside the enclosure on a u.FL-to-SMA pigtail with an IP67 bulkhead. Confirm the DC-DC converter is rated for at least the 2 amp peak current the Notecard can draw during a session, and that the input fuse is sized accordingly. |
-| Skylo satellite session times out, or no NTN session ever completes | GPS fix unavailable, antenna obstructed, or location not pinned to fixed mode | Confirm both the satellite and GNSS antennas are mounted outdoors (satellite signals do not penetrate a metal enclosure). Issue `{"req":"card.location.mode","mode":"fixed","lat":...,"lon":...}` once at commissioning so the Notecard does not cold-start the GPS each session. Allow 5 to 15 minutes on first power-on for the initial NTN registration. See the [satellite best practices guide](https://dev.blues.io/starnote/satellite-best-practices/). |
-| Intermittent VE.Direct parse failures (some windows produce sentinels, others succeed) | Long cable runs, ground loops between the battery shunt and the Notecarrier, or excessive noise coupling onto the RX line | Keep VE.Direct cables under the supplied 1.8 meter length, avoid running them parallel to high-current DC bus bars, and connect the battery-bus return to signal ground at one point only (not both at the converter and at the shunt). If runs cannot be shortened, add a small ferrite bead on the RX line near the Notecarrier CX header. |
-| Device goes silent after several days, never reappears in Notehub | Battery bank discharged below the converter's input cutoff, or the input fuse opened | The Traco TSR 1-2450 holds regulation down to 6.5 volts input; the TSR 1-4850 holds down to 18 volts. When the bus drops below cutoff the 5 volt rail collapses and the Notecarrier CX powers off. Once solar restores the bus, the device cold-starts and rejoins. Inspect the inline fuse and verify the battery bus is above the converter's minimum input. The last `_session.qo` timestamp tells you when the radio went silent. |
-
-If you encounter an issue not covered above, the [Blues community forum](https://discuss.blues.com) is the fastest path to a working answer; both Blues engineers and other field deployers actively monitor it.
-
-
-## 11. Limitations and Next Steps
-
-This reference design intentionally stays narrow: it answers the site-uptime question — "is this off-grid site healthy enough that I don't need to send a truck?" — and leaves the harder problems (cell-level diagnostics, MPPT control, multi-bank aggregation) to companion designs where they belong.
-
-### Simplified for the POC
-
-The simplifications below are deliberate scope choices: each marks where this site-uptime monitor stops and a companion design or firmware extension picks up.
-
-**Read-only over VE.Direct.** The firmware listens to the broadcast text protocol but never writes a command back to either Victron device. Setting parameters on the SmartShunt or MPPT (battery capacity, absorption voltage, equalization schedule) is done with the Victron VictronConnect app at commissioning, not from Notehub. Adding write support would mean tracking the proprietary VE.Direct HEX framing on top of the text protocol covered here.
-
-**Single battery bank, single MPPT.** The reference design assumes one SmartShunt watching one battery bank and one SmartSolar MPPT watching one solar array. Sites with parallel banks or multiple charge controllers require either a Victron Cerbo GX (which aggregates over Victron's CAN bus) or a firmware extension that opens additional VE.Direct serial ports on the Cygnet host.
-
-**No MPPT control.** The firmware reports what the MPPT is doing but cannot command a different absorption voltage, force a manual equalize, or disable charging. Remote control of the charge algorithm is a different problem class with safety implications and is intentionally out of scope.
-
-**Bank-level signal scope only.** Per-cell voltages and cell imbalance are not exposed on the VE.Direct wire; they travel over CAN bus and require dedicated hardware, as called out in §1. This design targets the bank-level uptime failure modes and explicitly defers cell-level telemetry to a companion design.
-
-**Sensor selection is fixed at the firmware level.** The two VE.Direct ports are hard-coded to a SmartShunt on Serial1 and an MPPT on the SoftwareSerial pin. Swapping in a different VE.Direct device family (such as a Phoenix inverter) requires extending the parser to recognize its label set.
-
-**No local sample history.** The host accumulates the current summary window in the persistent state struct between sleeps, but a power interruption that drains the Notecard backup capacitor clears the in-progress accumulator. Long-term storage lives in Notehub, not on the device.
-
-### Production Next Steps
-
-Taking this monitor toward a production fleet rollout means scaling to multi-bank sites, adding local alerting and field provisioning, and supporting over-the-air updates for devices that may go years between physical visits. The following extensions are the natural progression.
-
-**Multi-bank support.** Open additional SoftwareSerial ports on free Cygnet GPIO pins and parse a second SmartShunt and MPPT pair, adding a per-bank index to each Note so downstream analytics can attribute readings correctly.
-
-**Alarm output relays.** Drive a Cygnet GPIO to a small SSR or low-side switch, so a `harvest_deficit` or `soc_low` alert can trigger a local audible or visual indicator at the site for technicians on the ground.
-
-**Weather-data overlay.** Route `solar_summary.qo` events into a server-side process that joins each window's `yield_kwh` against a weather feed (cloud cover, GHI) for the site coordinates. A real recharge-deficit problem looks very different from a stretch of cloudy weather, and the overlay separates the two automatically.
-
-**BLE-based field provisioning.** A Bluetooth provisioning page on the Notecarrier CX would let a technician set the site identifier, fixed GPS coordinates, and initial thresholds from a phone in the field, with no laptop or USB cable required. The Notecard supports BLE on supported SKUs.
-
-**Notecard Outboard DFU for fleet firmware updates.** [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) lets Notehub push a new STM32 firmware image to every device in a fleet over the air. This is essential for a device that may live on a remote tower or backcountry array for years between physical visits.
-
-**Multi-phase or DC-coupled load shunting.** Sites that include AC inverter loads (Victron MultiPlus or similar) can add a Quattro or MultiPlus VE.Bus integration to capture inverter input, output, and inverter-internal alarms — a richer picture of total site load than the algebraic `pv_w - bat_w` computation used here.
-
-
-## 12. Summary
-
-The on-call engineer responsible for a remote tower or backcountry array now has something they didn't have before: days of warning. A `harvest_deficit` Note arrives in the NOC with enough context — consecutive deficit windows, average SoC, today's yield — to dispatch a truck before the bank empties rather than after the site has already gone dark. The Notecarrier CX, one Notecard, and the two Victron VE.Direct devices that are already on the wall do all the work, with the same firmware reaching Notehub over cellular where it can and over Skylo NTN satellite where it can't.
-
-The same pattern carries over to anywhere a VE.Direct device, a Modbus-RTU sensor, or a simple analog signal needs to reach the cloud from a site with no IT infrastructure of its own.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/off-grid-solar-battery-site-controller/)**
diff --git a/83-off-grid-solar-battery-site-controller/banner.png b/83-off-grid-solar-battery-site-controller/banner.png
deleted file mode 100644
index ac7696b4..00000000
Binary files a/83-off-grid-solar-battery-site-controller/banner.png and /dev/null differ
diff --git a/83-off-grid-solar-battery-site-controller/diagrams/01-system-architecture.svg b/83-off-grid-solar-battery-site-controller/diagrams/01-system-architecture.svg
deleted file mode 100644
index ff9fbf8e..00000000
--- a/83-off-grid-solar-battery-site-controller/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/83-off-grid-solar-battery-site-controller/diagrams/02-wiring-assembly.svg b/83-off-grid-solar-battery-site-controller/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index fef65dbb..00000000
--- a/83-off-grid-solar-battery-site-controller/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,75 +0,0 @@
-
diff --git a/83-off-grid-solar-battery-site-controller/diagrams/03-data-flow.svg b/83-off-grid-solar-battery-site-controller/diagrams/03-data-flow.svg
deleted file mode 100644
index 5ab3d1fd..00000000
--- a/83-off-grid-solar-battery-site-controller/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,61 +0,0 @@
-
diff --git a/84-remote-cabinet-backup-battery-sentinel/README.md b/84-remote-cabinet-backup-battery-sentinel/README.md
index 660fe74b..750db591 100644
--- a/84-remote-cabinet-backup-battery-sentinel/README.md
+++ b/84-remote-cabinet-backup-battery-sentinel/README.md
@@ -1,525 +1,5 @@
-# Remote Cabinet Backup Battery Sentinel — Pack-Level 12 V / 24 V Battery Monitor
+# Remote Cabinet Backup Battery Sentinel
-
-
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Remote%20Cabinet%20Backup%20Battery%20Sentinel%20%E2%80%94%20Pack-Level%2012%20V%20%2F%2024%20V%20Battery%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/cabinet_battery_sentinel/](firmware/cabinet_battery_sentinel/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a pack-level sentinel — a Blues [battery management systems](https://blues.com/battery-management-systems/) reference design — for the backup battery inside a **traffic-signal controller, roadside IoT gateway, industrial RTU, or equipment cabinet** running on a **12 V or 24 V positive-referenced DC bus**. A Blues [Notecard Cell+WiFi](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) and [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link), paired with a precision current/voltage monitor and a surface-mounted thermistor, continuously measure pack voltage, bidirectional current, surface temperature, and state-of-charge — with proactive alerts on charger faults, elevated float current (a reliable early indicator of **VRLA** capacity degradation weeks before failure. See §8 for chemistry-specific caveats), mains power loss detected within one sample interval, and a low-SoC threshold trip. Over cellular, independent of every piece of equipment the battery is supposed to protect.
-
-## 1. Project Overview
-
-**The problem.** A municipal traffic engineer responsible for hundreds of signalized intersections faces a quiet, expensive problem: the sealed backup battery inside every cabinet — a VRLA (valve-regulated lead-acid) or LFP (lithium iron phosphate) pack designed to keep the signal running through a mains outage — is almost universally untested until the day it fails. A healthy-looking battery can have lost 60% of its usable capacity to sulfation while still holding nominal open-circuit voltage. A single shorted cell in a 12 V six-cell VRLA string depresses the pack voltage, but the charger compensates by pushing more current, masking the fault entirely from any simple voltage-only check. The same pattern plays out in roadside IoT gateways, industrial RTU cabinets, and telecom shelters — whoever owns the site only finds out the battery is dead at the worst possible moment.
-
-The failure signal that *does* show up early is float current. A healthy VRLA battery at full charge draws only a few milliamps per 100 Ah from the charger — just enough to overcome self-discharge and electrochemical leakage. When internal resistance climbs due to sulfation or plate damage, the charger must push more current continuously just to hold voltage. Elevated float current is a reliable weeks-in-advance indicator that a battery is headed toward failure. It is a **pack-level aggregate** — it reveals that the string as a whole is degrading, which is often caused by one or more weakened cells, but it does not identify the specific cell or quantify per-cell imbalance. Distinguishing individual failed cells requires per-cell voltage monitoring hardware not included in this design (see §10). Nobody is watching pack float current, because nobody has instrumented it.
-
-This project instruments it. A precision bidirectional current monitor sits in series with the battery's positive terminal, measuring float current with milliamp resolution. When the charger current reverses and the battery starts discharging — because mains power failed — the Notecard fires an immediate alert. When float current climbs steadily over weeks, the hourly summary captures the trend for a downstream analytics system to act on.
-
-**Chemistry scope.** The float-current capacity-degradation signal is a VRLA mechanism. LFP batteries do not sulfate, and elevated float current on an LFP pack indicates a charger or BMS configuration fault rather than cell-level degradation. LFP installations still benefit fully from four of the six battery-condition alert modes this design provides: power-outage detection (current reversal), pack voltage bounds checking, surface temperature alerting, and low-SoC threshold tripping. Float-current trending as a capacity-health proxy, however, does not apply to LFP without chemistry-specific calibration. See §7 for per-alert chemistry guidance and §9 for LFP threshold commissioning Notes.
-
-**Why Notecard.** The Notecard's independence from site infrastructure is the fundamental feature here. A traffic-cabinet controller, a roadside LoRaWAN gateway, or a roadside remote terminal unit all have their own modems and radios, but those are exactly the devices the backup battery is supposed to keep running during a mains failure. You cannot use the site's LTE modem to report that the site's LTE modem just went down because the backup battery was dead.
-
-
-
-The Notecard manages its own cellular session against the supported carrier networks worldwide via its embedded global SIM, on a power path independent of the site equipment. When mains fails, the sentinel continues running from the cabinet battery bus through the DC-DC converter — which is precisely what allows it to observe the discharge in real time: voltage sag under actual site load, current draw, duration, and eventual recovery. The Notecarrier CX's onboard LiPo charger adds a reporting tail for the end-of-discharge case: once the cabinet battery is deeply depleted, the bus falls below the DC-DC converter's minimum input voltage, or the battery is disconnected entirely, the 2000 mAh LiPo takes over and extends cellular reporting beyond the battery's own capacity. If the sentinel needs to be fully independent of the monitored battery from the moment of mains failure, not riding on it at all — a separate, isolated power source is required. That discharge curve — only available because the sentinel kept running while everything else went dark — is the most valuable diagnostic data you can collect about backup battery health.
-
-**Deployment scenario.** A compact enclosure mounted inside a traffic-signal controller housing, roadside IoT gateway enclosure, industrial RTU cabinet, or telecom equipment shelter — installations running on a 12 V or 24 V positive-referenced DC bus. The Adafruit INA228 shunt monitor wires in series with the battery's positive terminal; the NTC thermistor is affixed to the battery case surface with thermal adhesive. A DC-DC step-down module converts the cabinet bus voltage to regulated 5 V for the Notecarrier CX; a 2000 mAh LiPo on the JST connector is charged from that 5 V supply; during a mains outage the sentinel continues running from the cabinet battery via the DC-DC converter, with the LiPo taking over only when the monitored battery is deeply depleted or the bus drops below the converter's input range. The Notecard's cellular antenna routes out of the enclosure via u.FL pigtail to a patch or magnetic-mount antenna on the cabinet exterior.
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Every two minutes the Cygnet STM32L433 host on the Notecarrier CX wakes for a few seconds, samples the battery, decides whether anything has gone wrong, and goes back to sleep. In those seconds it reads pack voltage and bidirectional current from the INA228 over Qwiic, picks up surface temperature from the NTC thermistor on A0, and evaluates six battery-condition rules plus one sensor-health check. Any tripped rule becomes an alert [Note](https://dev.blues.io/api-reference/glossary/#note) marked `sync:true` for immediate delivery. Window statistics accumulate in a state struct that `NotePayloadSaveAndSleep` writes into Notecard flash before [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) cuts host power entirely between samples. Window-average power (`voltAvg × currAvg`) is derived at summary time, not sampled per-read from the INA228 power register.
-
-**Notecard responsibilities.** The Notecard owns the radio so the host never has to. It manages its own cellular session against supported carrier networks worldwide via the embedded global SIM, queues Notes in on-device storage, opens a session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes), and short-circuits that cadence whenever a `sync:true` alert lands; those go out immediately. On the inbound side it pulls down [environment variable](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) updates from the [Blues Notehub](https://blues.com/notehub/) cloud service, so the field operator can retune any threshold or either cadence without reflashing.
-
-**Notehub responsibilities.** Everything that leaves the cabinet lands in [Notehub](https://notehub.io), which ingests and stores each event and runs the project's routes. The two Notefiles — `battery_summary.qo` for periodic telemetry and `battery_alert.qo` for threshold trips — stay deliberately separate so a route can fan summaries into a long-term analytics store while pushing alerts straight to whoever is on call.
-
-**Routing to the cloud (high level only).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-## 3. Technical Summary
-
-1. **Notehub:** create a [Notehub project](https://notehub.io) and copy the ProductUID.
-2. **Wire the bench rig:** Notecarrier CX + Notecard MBGLW + INA228 on Qwiic + NTC divider on A0 + LiPo on JST. Full pinout in [§5](#5-wiring-and-assembly).
-3. **Edit one line:** set `PRODUCT_UID` in [`firmware/cabinet_battery_sentinel/cabinet_battery_sentinel_helpers.h`](firmware/cabinet_battery_sentinel/cabinet_battery_sentinel_helpers.h).
-4. **Flash via CLI:**
- ```bash
- arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/cabinet_battery_sentinel/
- arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/cabinet_battery_sentinel/
- ```
- Or use Arduino IDE (Tools → Board → Cygnet; Upload).
-5. **Watch for success** — open Notehub → **Events** tab. You know it's working when:
- - `_session.qo` appears within **2–3 minutes**; this confirms the Notecard reached Notehub over cellular
- - `battery_summary.qo` appears within **60 minutes**; this is your hourly health summary (sample JSON below)
- - `battery_alert.qo` appears immediately if any alert condition trips (sample JSON below)
-
- If you don't see `_session.qo` after 5 minutes, check your PRODUCT_UID matches your Notehub project exactly and verify cellular coverage at your location. See §10 Troubleshooting if the Notecard never reaches Notehub.
-
- Example `battery_summary.qo` (healthy 12 V VRLA at float):
- ```json
- {
- "volt_v": 13.65,
- "curr_ma": 12.5,
- "power_mw": 170.6,
- "charge_ah": 0.0125,
- "soc_pct": 97.3,
- "temp_c": 24.3,
- "volt_min_v": 13.52,
- "curr_min_ma": 0.0,
- "temp_max_c": 25.1,
- "samples": 30
- }
- ```
-
- Example `battery_alert.qo` (power outage event):
- ```json
- {
- "alert": "power_outage",
- "volt_v": 12.4,
- "curr_ma": -3200.0,
- "temp_c": 25.5
- }
- ```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Compact carrier with an embedded Cygnet STM32L433 host MCU, Qwiic I2C port, and an onboard JST LiPo charger circuit — essential for keeping the sentinel powered through the outages it's built to detect. Accepts regulated 4.5–5.5 V on the +VBAT header pin. |
-| [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular uplink that manages its own cellular session against supported carrier networks worldwide via embedded global SIM; WiFi acts as an opportunistic fallback for sites that happen to have rooftop coverage. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | **BENCH VALIDATION ONLY** — not deployed in production. Splice inline on the 5 V power path during bring-up to measure the complete device's idle baseline, sensor-wake current, and cellular-session bursts. See §8 for analysis and §4 for wiring. Remove before field deployment. |
-| [Adafruit INA228 Power Monitor Breakout (#5832)](https://www.adafruit.com/product/5832) | 1 | 20-bit I2C precision monitor measuring bus voltage up to 85 V and bidirectional current through its onboard 15 mΩ shunt. The onboard shunt handles up to approximately 8 A continuous — appropriate for installations where peak discharge current does not exceed that limit (typically batteries up to roughly 40 Ah at a C/5 discharge rate). For higher-current installations, use the breakout's external-shunt footprint with a lower-value busbar shunt. |
-| 10 kΩ NTC thermistor, β=3950, waterproof probe | 1 | Surface-mounted to the battery case; detects elevated case temperature that can signal charging faults, poor cabinet ventilation, or abnormal battery conditions — and provides a per-sample temperature for the hourly summary. |
-| 10 kΩ 1% resistor | 1 | Pull-up resistor in the thermistor voltage divider. |
-| 3.7 V 2000 mAh LiPo battery, JST PH connector (e.g. [Adafruit #2011](https://www.adafruit.com/product/2011)) | 1 | Reporting-tail backup for the end-of-discharge case. During a normal mains outage the sentinel runs from the cabinet battery bus via the DC-DC converter; the LiPo takes over only after the monitored battery is deeply depleted or the bus drops below the converter's minimum input. The Notecarrier CX charges it from the regulated 5 V supply and switches automatically. |
-| 5 V regulated DC-DC step-down module — **12 V systems:** [RECOM R-78E5.0-1.0](https://www.digikey.com/en/products/detail/recom-power/R-785-0-0-5/2256217) (8–28 V input, 1 A) | 1 | Converts the 12 V cabinet bus to the regulated 4.5–5.5 V required by the Notecarrier CX +VBAT pin. The 8–28 V input range comfortably covers a 12 V VRLA or LFP charger bus at float and absorption. |
-| 5 V regulated DC-DC step-down module — **24 V systems:** [Pololu D24V50F5](https://www.pololu.com/product/2851) (6–38 V input, 5 A) | 1 | Use in place of the RECOM module when the cabinet bus is 24 V nominal (float ~27–28 V, absorption up to ~29 V). The 6–38 V input range covers 24 V VRLA and LFP charger buses with ample headroom. |
-| Inline fuse holder with 1 A 32 V blade fuse (ATC/ATM automotive style or equivalent) | 1 | **Safety-critical.** Install in series on the positive-bus cable between the battery bus (+) and the DC-DC converter input. Sized for 1 A — well above the sentinel's ≤500 mA peak draw from a 12 V or 24 V bus during a cellular session, and will clear before wiring fault damage. The INA228 shunt path (battery positive to load bus) carries the full site load current and must be installed on a battery branch that is already protected by an upstream circuit breaker or fuse rated for the expected load current — see §5. |
-| u.FL to SMA bulkhead pigtail, ~150 mm (e.g. [Adafruit #851](https://www.adafruit.com/product/851)) | 1 | Routes the Notecard's cellular antenna connection from the u.FL footprint on the Notecarrier CX to an SMA bulkhead fitting in the enclosure wall. Uses RG178 coax; verify the SMA connector sex matches your antenna before ordering. |
-| LTE cellular antenna with SMA connector, panel-mount or magnetic-mount (e.g. [SparkFun CEL-16432](https://www.sparkfun.com/products/16432), 698 MHz–2.7 GHz, 2.3 dBi) | 1 | Mount on the cabinet exterior for reliable LTE Cat-1 bis coverage. A rubber-duck or internal antenna inside a sealed metal enclosure will not maintain consistent signal. |
-| Qwiic cable, 100 mm or 200 mm | 1 | Connects the INA228 breakout to the Notecarrier CX Qwiic port. |
-| ABS enclosure, IP54 or better (e.g. [Hammond 1591XXTSFLBK](https://www.hammfg.com/part/1591XXTSFLBK), ~123 × 83 × 61 mm) | 1 | Houses the Notecarrier CX assembly inside the cabinet. The 1591XXTSFLBK provides ample internal volume for the board stack and wiring, with a clear polycarbonate lid for visual status checks without opening. Add cable glands for the antenna pigtail, thermistor probe, shunt wires, and power leads. |
-| Thermal adhesive tape (e.g., 3M 8810) | 1 | Affixes the thermistor probe firmly to the battery case surface for accurate temperature readings. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service: no activation fees, no monthly commitment.
-
-### Safety and Installation Requirements (Read Before Wiring)
-
-**Safety-critical step: inline fuse.** Install a **1 A fuse in series on the positive-bus cable between the battery bus (+) and the DC-DC converter input** (the sentinel's own power supply). Battery strings and busbars can deliver thousands of amperes of short-circuit current; the fuse sized for 1 A — well above the sentinel's ≤500 mA peak draw during a cellular session — will clear a wiring fault before causing arc damage or fire.
-
-**INA228 shunt installation.** The INA228 sits **in series on the battery's positive terminal** and must be installed on a battery branch that already has an **upstream circuit breaker or fuse rated for the expected load current**. Do not install the INA228 shunt on a battery segment with no existing overcurrent protection.
-
-**Lockout/tagout.** Before making any connections to an energised battery bus: isolate and lock out the battery charger and all parallel discharge paths (lockout/tagout procedure), use insulated tools, remove metallic jewelry and watchbands, and confirm the bus is de-energized before touching conductors. For cabinet installations with multiple parallel battery strings, all strings must be isolated simultaneously before working in the positive-bus circuit.
-
-
-
-This BOM targets a **positive-referenced DC bus** — the INA228 high-side sensing topology and the non-isolated 5 V step-down converter both reference the negative bus as circuit ground. It covers **12 V and 24 V positive-referenced** VRLA and LFP charger buses (e.g., 12 V VRLA at 13.5–14.7 V float; 24 V VRLA at 27–29.4 V float). For 24 V systems, update `volt_min_v` and `volt_max_v` to match your charger's float window — see §6 — and use the Pololu D24V50F5 listed in the BOM. Also verify that the expected peak discharge current is within the shunt's rating (≤8 A for the onboard 15 mΩ shunt; use an external shunt for higher-current installations).
-
-
-
-## 5. Wiring and Assembly
-
-
-
-
-
-**Safety reminder:** Review the safety requirements in §3 before proceeding. Ensure the inline fuse is installed on the sentinel's power supply and the INA228 shunt is on a battery branch with upstream overcurrent protection.
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header and Qwiic connector. The Notecard Cell+WiFi seats into the M.2 slot.
-
-**Cabinet bus to Notecarrier CX power chain:**
-
-The cabinet battery bus (typically 12–15 V for 12 V VRLA/LFP, or 25–29 V for 24 V VRLA/LFP charger buses) is not directly compatible with the Notecarrier CX's +VBAT input, which requires regulated 4.5–5.5 V. A DC-DC step-down module bridges the two:
-
-```
-Cabinet battery bus (+) → 1 A inline fuse → DC-DC module input (+)
-DC-DC module output (5 V regulated) → Notecarrier CX +VBAT header pin
-Cabinet battery bus (–) / cabinet ground → Notecarrier CX GND header pin
-```
-
-Mount the DC-DC module inside the enclosure and verify its input-voltage range covers your cabinet supply before wiring. The Notecarrier CX's USB-C connector is an equivalent 5 V power entry point if a USB-C cable is more convenient than the header pin for your enclosure layout. See the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) for +VBAT and GND pin locations on the dual 16-pin header.
-
-The 2000 mAh LiPo on the JST connector is charged from the regulated 5 V supply whenever the cabinet bus is present. During a normal mains outage the sentinel continues drawing power from the cabinet battery via the DC-DC converter — the LiPo does not take over yet. This is intentional: the sentinel is riding on the battery it monitors, directly observing the discharge as it happens. The LiPo becomes the primary power source only after the monitored battery is deeply depleted, the bus voltage drops below the DC-DC converter's minimum input threshold, or the battery is disconnected — extending the reporting tail beyond the battery's own capacity. If the design requirement is full electrical independence from the monitored battery from the moment mains fails, an isolated power supply fed from a separate source is needed instead.
-
-**INA228 battery power path (high-side current sensing):**
-
-The INA228 measures the current flowing in or out of the battery by sitting in series with the battery's positive terminal; this topology is called high-side sensing.
-
-- **Battery (+) terminal** → **INA228 `V+` pad** (connects to the shunt IN+ input)
-- **INA228 `V–` pad** → cabinet load bus (+)
-- **Battery (–) terminal** → cabinet load bus (–) / circuit ground
-
-The 15 mΩ shunt is internal to the Adafruit #5832 breakout; no external shunt resistor is required for currents up to approximately 8–10 A continuous. For installations where peak discharge current exceeds 8 A — a 100 Ah battery at a C/5 discharge rate draws 20 A — bypass the internal shunt via the breakout's external-shunt footprint and wire a lower-value shunt (e.g. 1–2 mΩ), updating `INA228_SHUNT_OHMS` and `INA228_MAX_CURRENT_A` in firmware to match. See §9 (Limitations) for further details.
-
-**INA228 to Notecarrier CX (I2C via Qwiic):**
-
-- **INA228 Qwiic connector** → **Notecarrier CX Qwiic connector** (3.3 V, GND, SDA, SCL; Qwiic cable)
-
-The Notecarrier CX has onboard I2C pull-ups; no additional pull-up resistors are required.
-
-**NTC thermistor voltage divider (A0):**
-
-- **Notecarrier CX +3V3** → **10 kΩ 1% resistor** → **Notecarrier CX A0**
-- **Notecarrier CX A0** (same node as above) → **NTC thermistor leg 1**
-- **NTC thermistor leg 2** → **Notecarrier CX GND**
-
-Affix the thermistor probe to the battery case surface using thermal adhesive tape. For multi-cell packs, position it near the geometric center of the pack where heat from an internal fault tends to be highest.
-
-**LiPo backup battery:**
-
-- **2000 mAh LiPo** → **Notecarrier CX JST PH battery connector**
-
-The Notecarrier CX charges the LiPo from the regulated 5 V supply on +VBAT. The transition from external power to LiPo is automatic; no firmware change is needed.
-
-**Notecard:**
-
-Insert the Notecard Cell+WiFi into the Notecarrier CX M.2 slot and secure with the mounting screw. Connect the u.FL to SMA pigtail to the antenna footprint on the Notecard, route it to an SMA bulkhead fitting in the enclosure wall, and attach the external antenna on the cabinet exterior. A rubber-duck antenna inside a sealed steel cabinet will not maintain reliable LTE Cat-1 bis coverage.
-
-**Mojo (bench validation only):**
-
-For bench bring-up, splice the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the **5 V DC-DC module output** and the Notecarrier CX +VBAT input — do not connect it directly to a 12 V or 24 V cabinet bus. Mojo measures the current consumed by the entire assembled device on the 5 V power rail.
-
-**I²C topology:** this firmware does not read Mojo's coulomb counter over I²C; Mojo is inline power measurement only. The INA228 connects directly to the Notecarrier CX Qwiic connector as described above — Mojo's Qwiic port does not need to be wired for this bring-up.
-
-Remove Mojo from the power path for production deployment; see §9.
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid); it looks like `com.your-company.your-name:cabinet-battery-sentinel`.
-
-2. **Set the ProductUID in firmware.** Open [`cabinet_battery_sentinel_helpers.h`](firmware/cabinet_battery_sentinel/cabinet_battery_sentinel_helpers.h) and replace the empty string on the `#define PRODUCT_UID ""` line with your value.
-
-3. **Claim the Notecard.** Power the assembled unit. On first cellular connection the Notecard associates with your Notehub project automatically. The device appears in the **Devices** tab within a few minutes; no manual claim step is required.
-
-4. **Create a Fleet.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared environment variable configuration and routing. The natural organization here is by battery chemistry and voltage — one fleet for 12 V VRLA roadside cabinets, another for 24 V VRLA or LFP installations — because the safe float voltage windows differ materially between chemistries. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can automate fleet assignment as devices provision.
-
-5. **Set environment variables.** In Notehub, navigate to **Fleet → Environment** (or **Device → Environment** for a per-unit override). Add any of the variables below. The Notecard pulls them on its next inbound sync; no reflash, no truck roll. All are optional — firmware defaults apply when a variable is absent.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `sample_interval_sec` | `120` | Seconds between sensor wakes. Min 30, max 3600. At 120 s the device takes 30 samples per hourly summary. |
- | `summary_interval_min` | `60` | Minutes between `battery_summary.qo` Notes. Changing this value also re-applies `hub.set` on the next wake so the Notecard's outbound session cadence stays in sync with the new summary interval. |
- | `volt_min_v` | `13.2` | Pack voltage (V) below which `float_voltage_low` fires. **Default calibrated for 12 V VRLA** — the low end of the normal float window is approximately 13.2–13.5 V (2.20–2.25 V/cell). For 24 V systems set to ~26.4; for LFP systems, set chemistry-appropriate thresholds per the Chemistry-Specific Alert Notes in §7. |
- | `volt_max_v` | `14.8` | Pack voltage (V) above which `float_voltage_high` fires. **Default calibrated for 12 V VRLA** — typical float is 13.5–13.8 V; the default gives 1 V of margin above the absorption ceiling. For 24 V systems set to ~29.0; for LFP, see §8. |
- | `float_current_hi_ma` | `500` | Float current (mA) above which `float_current_high` fires. **Default calibrated for 12 V VRLA** — a healthy 100 Ah VRLA draws <50 mA at float; 500 mA is a clear anomaly signal. See the "Chemistry-Specific Alert Notes" sidebar in §7 for VRLA vs. LFP interpretation and commission a chemistry-appropriate threshold at installation. |
- | `temp_alert_c` | `40` | Pack surface temperature (°C) above which `temp_high` fires. VRLA batteries age roughly 2× faster for every 10 °C above 25 °C; 40 °C is the standard service-alert threshold. |
- | `discharge_ma` | `-200` | Current (mA) below which `power_outage` fires. −200 mA provides a clear margin above float-current noise while catching any sustained discharge within a single sample. |
- | `usable_capacity_ah` | `100` | Battery bank usable capacity (Ah) used for coulomb-counting SoC. Set this to the battery's nameplate capacity (or measured usable capacity from a full-discharge cycle). Required for meaningful `soc_pct` values — the firmware uses the default 100 Ah until overridden. For a 200 Ah bank set to `200`. |
- | `soc_low_pct` | `20` | SoC (%) below which `soc_low` fires. |
- | `soc_pct_init` | *(unset)* | Initial SoC (%) to apply the **first time** this value is seen after a known-full charge. Set once after confirming the battery is fully charged (charger holding float voltage and current at minimum); the firmware tracks SoC from that baseline. To recalibrate: change the value to the new known SoC — the firmware detects the change and re-initializes on the next inbound sync. Leave unset (or set to `−1`) to defer commissioning; `soc_pct` in summary notes will carry `−9999` until a value is applied. |
-
-6. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) targeting `battery_alert.qo`; this is your real-time channel to an on-call queue, CMMS ticket, or alerting platform. Add a second route for `battery_summary.qo` to a time-series analytics store where float-current trends can be visualized over weeks and months. The two Notefiles are separate by design so they can be fanned to different destinations with different urgencies and retention policies.
-
-### Expected events in Notehub
-
-Within a minute of first power-on the **Events** tab should begin populating. Three event kinds matter:
-
-- **`_session.qo`** — automatic Notecard housekeeping on each cellular session; confirms the radio is reaching Notehub.
-- **`battery_summary.qo`** — one per `summary_interval_min`. `charge_ah` is the net coulombs over the summary window: positive values during float operation, large negative values during site discharge. `soc_pct` is the running coulomb-counted state-of-charge estimate; `−9999` means `soc_pct_init` has not yet been commissioned. `curr_min_ma` tracks the deepest discharge current (most-negative value) seen in the window; `0.0` means no discharge occurred. See the Quickstart section for full example JSON.
-
-- **`battery_alert.qo`** — emitted only on a threshold trip or sensor fault, transmitted immediately. The `alert` field is one of `float_voltage_low`, `float_voltage_high`, `float_current_high`, `temp_high`, `power_outage`, `soc_low`, or `ina228_unreachable`. See the Quickstart section for example JSON.
-
-## 7. Firmware Design
-
-The firmware is split across three files in `firmware/cabinet_battery_sentinel/`:
-
-| File | Contents |
-|---|---|
-| `cabinet_battery_sentinel.ino` | Includes, global variable definitions, `setup()`, `loop()` |
-| `cabinet_battery_sentinel_helpers.h` | All `#define` constants (including `PRODUCT_UID`), `SentinelState` struct, extern globals, function prototypes |
-| `cabinet_battery_sentinel_helpers.cpp` | All helper function implementations |
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — install via the Arduino Boards Manager (add the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**). Select **Blues Cygnet** as the board (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-- **`Blues Wireless Notecard`** (note-arduino): install via the Arduino Library Manager (`arduino-cli lib install "Blues Wireless Notecard"`). Check [note-arduino releases](https://github.com/blues/note-arduino/releases) and use the latest stable version.
-- **`Adafruit INA228`**: install via the Arduino Library Manager (`arduino-cli lib install "Adafruit INA228"`).
-- **`Adafruit BusIO`**: required dependency of the INA228 library; install via Library Manager.
-
-**Before compiling**, open `cabinet_battery_sentinel_helpers.h` and replace the empty string on the `#define PRODUCT_UID ""` line with your Notehub ProjectUID. All three source files in the sketch folder are compiled together automatically by the Arduino build system.
-
-**Flashing via `arduino-cli`:**
-
-```bash
-# Confirm the correct FQBN for your installed core version
-arduino-cli board listall | grep -i cygnet
-
-# Compile and upload (replace FQBN and port with what the command above reports)
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/cabinet_battery_sentinel/
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/cu.usbmodem* \
- firmware/cabinet_battery_sentinel/
-```
-
-**Flashing via Arduino IDE:** open `cabinet_battery_sentinel.ino`, select the Cygnet board, select the correct port, and click **Upload**. The Notecarrier CX exposes the ST-Link interface on the same USB-C cable, so no external programmer is needed.
-
-After upload, open the serial monitor at **115200 baud**. On each wake the firmware prints one or two `[sentinel]` lines — for example:
-
-```
-[sentinel] volt=13.652V curr=12.5mA
-[sentinel] temp=24.3C
-```
-
-Then the host powers off for the sample interval and the monitor goes quiet. `INA228 FAIL` or `NTC open/shorted` messages indicate a wiring problem on those sensors. If the INA228 init fails, both INA228 readings (voltage and current) are skipped for that sample; the summary accumulates only valid samples.
-
-### 7.2 Modules
-
-| Responsibility | Where |
-|---|---|
-| Notecard config (`hub.set`, template, accelerometer) | `notecardConfigure`, `defineTemplate` (helpers.cpp) |
-| Env-var fetch and range clamp | `fetchEnvOverrides` (helpers.cpp) |
-| INA228 init and per-wake calibration | `initINA228` (helpers.cpp) |
-| Battery voltage and current reads | `readBatteryVoltage`, `readBatteryCurrent` (helpers.cpp) |
-| NTC thermistor temperature read | `readPackTempC` (helpers.cpp) |
-| Six battery-condition alert rules plus sensor-health fault, all with per-alert cooldowns | Inline in `setup()` (cabinet_battery_sentinel.ino) |
-| INA228 persistent-fault remote notification | Inline in `setup()` + `sendAlert` |
-| Hourly summary accumulation and emission | `sendSummary` (helpers.cpp) |
-| Immediate-sync alert emission | `sendAlert` (helpers.cpp) |
-| State persistence and host sleep | `sleepHost` → `NotePayloadSaveAndSleep` (helpers.cpp) |
-
-### 7.3 Sensor reading strategy
-
-**INA228 (voltage and current).** The INA228 loses power while the Cygnet is sleeping, so it's re-initialized on every wake with `begin()` followed by `setShunt(0.015, 8.0)`. The calibration call programs the chip with the shunt resistance and full-scale current. Single-point reads of `readBusVoltage()`, `readShuntVoltage()`, and `readCurrent()` take well under 10 ms. Current is *signed* at the firmware level: positive values mean the charger is supplying current, negative values mean the battery is discharging. This sign convention makes `power_outage` detection trivially simple: just a threshold comparison against `g_dischargeMa`.
-
-**Battery-terminal voltage.** With Battery(+) wired to INA228 V+ and the load/charger bus wired to V−, `readBusVoltage()` returns the voltage at V− (load side of the shunt), not the battery terminal. `readBatteryVoltage()` adds the shunt voltage to recover the true battery-terminal voltage: `V_terminal = V_bus + readShuntVoltage() / 1000`. The correction is ≤ 7.5 mV at float currents up to 500 mA (negligible) and reaches ~48 mV at 3.2 A discharge — material for voltage-alert accuracy and for the `volt_v` time series used in float-voltage trending.
-
-**Current sign note.** The INA228 is wired with Battery(+) on V+ and the load bus on V−, so the chip internally reports *positive* raw current during discharge (battery→load direction). `readBatteryCurrent()` negates the raw `readCurrent()` result before returning it, so all downstream comparisons, accumulations, and alert thresholds use the intuitive convention (positive = charger present, negative = outage). The documented wiring in §4 and the semantic descriptions throughout this README all refer to the post-negation value.
-
-The INA228 has an internal power register, but this design does not read it; `power_mw` in the summary note is derived as `voltAvg × currAvg` in `sendSummary()`, a window-average approximation, not a per-sample hardware measurement.
-
-**NTC thermistor.** A 16-sample average of 12-bit ADC readings (`analogReadResolution(12)` on the Cygnet) reduces noise before applying the β-equation: `T = 1 / (1/T₀ + (1/β) × ln(R/R₀))`. ADC readings within 50 mV of the supply rails (indicating an open or shorted probe) return `NAN` and are excluded from the temperature accumulator. A bad temperature reading never silently biases the summary averages, and — because voltage, current, and temperature each maintain their own independent sum and valid-sample count — a failed thermistor probe never suppresses voltage or current accumulation, alert evaluation, or the `power_outage` detection path.
-
-**Charge balance and SoC.** The INA228's hardware charge accumulator resets every time the chip powers up, which happens on every wake cycle since the Cygnet's 3.3 V rail is gated. Instead, the firmware accumulates charge in software: `chargeAh += (curr_mA / 1000) × (sample_interval_sec / 3600)` each cycle. The per-window result appears as `charge_ah` in the summary note: a small positive value during normal float, a large negative value during a power outage. `charge_ah` is a **per-window delta, not state-of-charge**. State-of-charge is maintained separately in `soc_pct`: on each wake the same current-integration delta is divided by `usable_capacity_ah` and added to the running SoC estimate, which persists across sleep cycles in Notecard flash. `soc_pct` in the summary note carries `−9999` (SUMMARY_INVALID_SENTINEL) until the operator commissions a starting SoC via `soc_pct_init`; see §6 for commissioning steps and §9 for accuracy caveats.
-
-### 7.4 Event payload design
-
-`battery_summary.qo` is [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates), giving it a fixed wire schema and a ~3–5× smaller on-wire footprint than free-form JSON, material for a device that will send 24 notes per day for years. `battery_alert.qo` is untemplated and uses `sync:true` for immediate delivery.
-
-Field semantics:
-- `power_mw` is derived as `voltAvg × currAvg`: a window-average approximation, not a per-sample hardware read from the INA228 power register.
-- `charge_ah` is the **net coulombs** delivered to or drawn from the battery during the window: a per-window delta only, not running state-of-charge.
-- `soc_pct` is the **running state-of-charge estimate** maintained across windows by integrating current against the commissioned `usable_capacity_ah` baseline; `−9999` means not yet commissioned.
-- `curr_min_ma` tracks the most-negative (deepest discharge) current seen in the window; `0.0` means no discharge occurred; a negative value (e.g., `−3200.0`) means the battery was actively discharging at 3.2 A into the load.
-
-Example JSON is shown in the Quickstart section above.
-
-### 7.5 Low-power strategy
-
-The Cygnet is fully power-gated by [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) for the full 120-second sample interval. The host does not delay or MCU-sleep; it powers off entirely, leaving only the Notecard active at its own published ~8 µA idle current. This is a Notecard-datasheet figure; the actual current drawn from the 5 V supply during idle (as measured by Mojo on the assembled device) will be higher, because it includes the Notecard's ~8 µA plus the quiescent current of the DC-DC converter and Notecarrier CX regulators. Firmware state is serialized into Notecard flash by `NotePayloadSaveAndSleep` before the host shuts down, and deserialized by `NotePayloadRetrieveAfterSleep` on the next wake, so the rolling window accumulates correctly across hundreds of sleep cycles per day.
-
-The Notecard itself is set to `periodic` mode with `outbound:60`. Summary notes queue in the on-device flash store and ship in a single cellular session once per hour, keeping radio duty-cycle low. Alert notes with `sync:true` bypass the outbound timer and wake the radio immediately. But because alert cooldowns prevent re-firing for 30 minutes of wall-clock time (stored as remaining seconds in the persistent state and decremented by the sample interval on each wake), even a sustained fault doesn't generate continuous radio wakes, and the 30-minute window holds regardless of whether `sample_interval_sec` is tuned to 30 s or 2 min.
-
-### 7.6 Retry and error handling
-
-- **Notecard configuration.** The initial `hub.set` in `notecardConfigure()` uses `sendRequestWithRetry(req, 5)`: a 5-second retry window covers the cold-boot I2C race where the STM32 comes up before the Notecard has finished initializing. `note.template` registration checks the boolean return value of `sendRequest` and logs a debug message on failure. Both the clean-boot path and the invalid-state-segment recovery path call `doFirstBoot()` so hub configuration and the Note template are never left stale after a firmware update that changes the state struct layout.
-- **Environment variable fetch.** `fetchEnvOverrides()` calls `requestAndResponse` and inspects the `err` field before reading the body; a Notecard-side error (e.g. not yet associated with Notehub) returns early rather than silently leaving stale threshold values from a corrupted response. The `hub.set` re-apply block in `setup()` only updates `state.lastSummaryMin` after `sendRequest` confirms delivery, so a transient I2C fault doesn't desynchronize the recorded cadence from the Notecard's actual setting.
-- **INA228 fault.** `initINA228()` returns `false` on I2C NACK. Both INA228 readings (voltage and current) are set to `NAN`, excluded from their respective metric accumulators, and skipped in alert evaluation — the firmware continues to sleep rather than hanging on a sensor fault. Temperature accumulation and the `temp_high` alert continue independently. A persistent INA228 failure is surfaced remotely: when `initINA228()` returns `false` and the `coolInaFaultSec` cooldown has expired, an `ina228_unreachable` alert is emitted to `battery_alert.qo` (rate-limited to once per 30 minutes so a sustained hardware fault does not flood the notefile). When the INA228 is unreachable for an entire summary window, `sendSummary()` still emits the note — all INA228 fields carry `SUMMARY_INVALID_SENTINEL` and `samples` is 0 — so Notehub shows a visible fault window rather than a silent gap.
-- **NTC fault.** ADC readings within 50 mV of the supply rails return `NAN` and are excluded from temperature accumulation. When only the thermistor is faulted (INA228 data is still valid), the summary is emitted normally with `SUMMARY_INVALID_SENTINEL` (−9999) in the `temp_c` and `temp_max_c` fields so downstream analytics can distinguish "sensor failed" from a true near-zero reading. When the INA228 is also unreachable for the entire window, `sendSummary()` still emits — all INA228 fields carry `SUMMARY_INVALID_SENTINEL` and `samples` is 0; the `ina228_unreachable` alert provides the immediate notification while the sentinel-filled summary preserves time-series continuity.
-- **Note delivery.** `sendSummary()` and `sendAlert()` each retry `note.add` up to three times with a 500 ms delay between attempts. `sendSummary()` returns a boolean; metric accumulators and the window elapsed timer are only reset after a confirmed successful delivery; a transient Notecard I2C fault preserves the window data so the next wake retries with the data intact rather than losing the window silently.
-- **Alert gating.** `float_voltage_low`, `float_voltage_high`, and `float_current_high` are suppressed during active battery discharge (`curr < discharge_ma`) to prevent misleading float-fault alerts during legitimate power-outage events. `float_current_high` is additionally suppressed for 30 minutes after the last discharge sample (the `postDischargeSec` settling window in the state struct) so normal bulk-recharge current following an outage recovery is not misclassified as elevated float current.
-- **Env var range clamping.** `g_sampleSec` is clamped to [30, 3600] and `g_summaryMin` to [5, 1440] so a misconfigured variable cannot produce absurd sleep intervals or an unreachable summary window. When `sample_interval_sec` changes mid-window, `windowElapsedSec` is reset so the next summary covers exactly the newly configured interval rather than an unintended hybrid duration.
-- **All six battery-condition rules and the sensor-health alert** use separate cooldown counters and fire independently. A battery that is simultaneously low-voltage and overtemperature fires both alerts; neither suppresses the other.
-
-### 7.7 Key code snippet 1 — template registration
-
-The template registers the `battery_summary.qo` schema as a fixed-length wire record, compressing every Note to a fixed byte count for ~3–5× smaller over-the-air footprint. Format codes use the notation ``:
-- `14.2` = 4-byte IEEE-754 float, encoded to 2 decimal places on the wire (e.g. `13.65` V becomes two bytes instead of the full float)
-- `14.1` = 4-byte float, 1 decimal place (e.g., `12.5` mA)
-- `14.3` = 4-byte float, 3 decimal places (e.g., `0.0125` Ah)
-- `12` = 2-byte signed integer (e.g., `30` samples)
-
-Any subsequent `note.add` with a body that doesn't match this schema is rejected by the Notecard, catching firmware mistakes at the device rather than corrupting the downstream time series.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "battery_summary.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "volt_v", 14.2); // battery-terminal voltage (V)
-JAddNumberToObject(body, "curr_ma", 14.1);
-JAddNumberToObject(body, "power_mw", 14.1);
-JAddNumberToObject(body, "charge_ah", 14.3); // net coulombs this window (per-window delta)
-JAddNumberToObject(body, "soc_pct", 14.1); // running SoC estimate (%); -9999 = not commissioned
-JAddNumberToObject(body, "temp_c", 14.1);
-JAddNumberToObject(body, "volt_min_v", 14.2);
-JAddNumberToObject(body, "curr_min_ma", 14.1);
-JAddNumberToObject(body, "temp_max_c", 14.1);
-JAddNumberToObject(body, "samples", 12);
-notecard.sendRequest(req);
-```
-
-### 7.8 Key code snippet 2 — immediate power-outage alert
-
-`sync:true` tells the Notecard to wake the radio immediately instead of waiting for the next outbound window. An operator can be notified within one sample interval (120 seconds at default settings) plus cellular session-establishment time (~15–60 seconds typical on LTE Cat-1 bis) after a current reversal crosses the `discharge_ma` threshold, so roughly two to three minutes end-to-end from the moment mains fails to the alert landing in Notehub at default settings. Reduce `sample_interval_sec` via env var if a shorter detection window is needed.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "battery_alert.qo");
-JAddBoolToObject(req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "power_outage");
-JAddNumberToObject(body, "volt_v", volt);
-JAddNumberToObject(body, "curr_ma", curr);
-JAddNumberToObject(body, "temp_c", temp);
-notecard.sendRequest(req);
-```
-
-### 7.9 Key code snippet 3 — sleep and state persistence
-
-`NotePayloadSaveAndSleep` serializes the `SentinelState` struct into Notecard flash and issues a `card.attn` sleep command. On the next wake, `NotePayloadRetrieveAfterSleep` and `NotePayloadGetSegment` restore the struct: rolling averages, window extremes, alert cooldowns, and charge balance all survive intact.
-
-```cpp
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, g_sampleSec, NULL);
-```
-
-## 8. Data Flow
-
-
-
-**Collected** every `sample_interval_sec` (default 120 seconds): pack voltage in V, float/discharge current in mA, and pack surface temperature in °C. Window-average power (mW) is derived as `voltAvg × currAvg` at summary time, not sampled directly from the INA228 power register.
-
-**Accumulated** in `SentinelState` across sleep cycles: rolling sums for window averages, the minimum voltage seen at a sample point (useful for detecting voltage sag, Note that sags entirely contained between two consecutive 2-minute samples will not be captured), the most-negative current (captures peak discharge rate during a power event), the maximum temperature, and the net charge balance in amp-hours.
-
-**Transmitted:**
-- `battery_summary.qo` — one record per `summary_interval_min` (default 24 per day), template-encoded. Window averages plus extremes. A healthy battery at float shows a small positive `charge_ah` proportional to float current and window duration (e.g., 0.0125 Ah for 12.5 mA average over one hour); a battery that spent time discharging shows a large negative `charge_ah`.
-- `battery_alert.qo` — emitted immediately on a threshold trip, with per-alert cooldowns preventing re-firing for approximately 30 minutes per alert type.
-
-**Routed:** Notehub fans both Notefiles to configured routes. Natural split: `battery_alert.qo` → real-time on-call, CMMS, or SMS gateway; `battery_summary.qo` → time-series database for multi-week float-current trend analysis.
-
-**Alert triggers** — six battery-condition rules plus one sensor-health alert:
-
-| Alert | Condition | Diagnostic meaning |
-|---|---|---|
-| `float_voltage_low` | Voltage < `volt_min_v` **while charger present** (`curr ≥ discharge_ma`) | Charger not supplying enough current, failed cell, or battery unable to reach float voltage under load. Suppressed during active discharge so a legitimate voltage sag while running on battery does not generate a spurious float-fault alert. |
-| `float_voltage_high` | Voltage > `volt_max_v` **while charger present** (`curr ≥ discharge_ma`) | Charger overvoltage fault; in VRLA, accelerates plate corrosion and electrolyte loss via gassing. Suppressed during active discharge for the same reason as `float_voltage_low`. |
-| `float_current_high` | Current > `float_current_hi_ma` **while charger present and post-discharge settling window expired** | Elevated float current. See "Chemistry-Specific Alert Notes" sidebar below. Suppressed during active discharge and for 30 minutes after the last discharge sample so normal bulk-recharge current following an outage recovery is not misclassified as float degradation. |
-| `temp_high` | Temperature > `temp_alert_c` | Elevated battery case temperature. A single surface thermistor cannot identify the heat source or confirm specific failure modes — it indicates the battery, charger, or enclosure is running abnormally warm. Sustained elevated temperature accelerates battery aging. Inspect charger output, enclosure ventilation, and battery condition. |
-| `power_outage` | Current < `discharge_ma` | Site is running on battery. The event the whole system was built to detect — and to survive long enough to report. |
-| `soc_low` | `soc_pct` < `soc_low_pct` **and SoC has been commissioned** | State-of-charge has fallen below the operator-configured threshold. Only fires after `soc_pct_init` has been set; `soc_pct` is included automatically in the alert note. Inspect whether a recent discharge event was deeper or longer than expected — a battery that drives SoC low during a brief outage has less reserve than its nameplate rating suggests. |
-| `ina228_unreachable` | INA228 `begin()` fails on I2C NACK | The primary current and voltage sensor is not responding. All INA228-derived telemetry is blind until the fault clears. Rate-limited to once per 30 minutes. Inspect Qwiic wiring, I2C address (A0/A1 jumpers), and 3.3 V supply. |
-
-#### Chemistry-Specific Alert Notes
-
-**VRLA batteries:** Elevated float current is a reliable early indicator of sulfation, internal resistance growth, or approaching end-of-life — typically weeks before a battery fails a load test. This is a **pack-level aggregate signal** indicating the string as a whole requires more continuous charge current than a healthy pack, consistent with one or more degraded cells, but does not identify which cell or confirm per-cell imbalance. **Default thresholds are calibrated for 12 V VRLA** (`volt_min_v=13.2`, `volt_max_v=14.8`, `float_current_hi_ma=500`). For 24 V VRLA, scale voltage thresholds proportionally (`volt_min_v≈26.4`, `volt_max_v≈29.0`).
-
-**LFP (lithium iron phosphate) batteries:** Elevated float current does *not* indicate cell degradation — LFP packs do not sulfate. On an LFP pack, elevated float current typically signals a charger or BMS configuration fault instead. Voltage monitoring, `power_outage` detection, and `temp_high` remain fully applicable. **Do not commission LFP systems on default VRLA thresholds**; set `volt_min_v`, `volt_max_v`, and `float_current_hi_ma` to values calibrated for your specific LFP charger curve (typically narrower than VRLA — e.g., float ~13.5–13.8 V, absorption ~14.6 V for most LFP). Commission thresholds at installation after observing your charger's normal operating range.
-
-## 9. Validation and Testing
-
-**Expected steady-state behavior.** A healthy backup battery generates approximately one `battery_summary.qo` per hour and zero `battery_alert.qo` events. The `_session.qo` session events confirm cellular connectivity is healthy. Expect a brief flurry of `battery_alert.qo` events during the first few days while thresholds are tuned to the specific battery and charger on site; watch one week of hourly summaries before treating the baseline as calibrated.
-
-**Simulating a power outage.** During bench bring-up the simplest test is to temporarily lower `discharge_ma` to `−5.0` via a Notehub environment variable. On the next inbound sync the Notecard pulls the new value; on the next sample, any measurement more negative than −5 mA fires `power_outage`. Restore the threshold after the test. Alternatively, physically disconnect the charger while monitoring the bench rig; the first sample after current reverses will emit an alert.
-
-**Using Mojo to validate power behavior.** The Notecard's published datasheet current figures — see the [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) for authoritative numbers across SKUs and modes:
-
-| Phase | Current @ 5 V |
-|---|---|
-| Idle baseline (host powered off, radio off) | ~8 µA (Notecard only) |
-| Host wake, sensor read (Cygnet + INA228 + ADC) | ~15–20 mA for <1 s |
-| Cellular session, LTE Cat-1 bis (one queued note) | ~200–300 mA average, 15–30 s |
-
-**Whole-device power profile.** The figures above for Notecard idle and cellular are datasheet specs. On the assembled device, the idle baseline (host powered off via `card.attn`, radio idle) measured on a 5 V power rail includes the Notecard's ~8 µA plus the DC-DC converter's quiescent draw (~2 mA for RECOM R-78E5.0-1.0 or Pololu D24V50F5) plus Notecarrier CX regulator quiescent — typically **2–5 mA total at no load**. Use the trace **shape** — a 2-minute wake blip (~15–20 mA for <1 s), an hourly burst (~200–300 mA for 15–30 s), and a flat floor in between — to confirm correct sleep/wake behavior. The Mojo is spliced on the 5 V power rail and measures the entire assembled device; focus on a *flat* baseline at the expected quiescent level for your DC-DC converter rather than chasing sub-milliamp absolutes.
-
-A useful Mojo trace pattern for a healthy unit:
-- **Blip every 2 minutes** (<1 s at roughly 15–20 mA) — Cygnet awake reading sensors
-- **Sustained burst once per hour** (15–30 s at 200–300 mA) — Notecard cellular session
-- **Flat baseline between ~2–5 mA** — host powered off, Notecard radio idle, floor dominated by the DC-DC converter's quiescent current. The RECOM R-78E5.0-1.0 draws approximately 2 mA at no load; the Pololu D24V50F5 is in the same range. Focus on a *flat* floor at the expected level for your converter rather than chasing a sub-milliamp absolute figure.
-
-> **Baseline measurement conditions.** For the idle-current floor to reflect deployment conditions, power the device through the DC-DC module into the +VBAT header with **no USB cable attached**. A connected USB cable can prevent the Cygnet STM32's USB peripheral and the Notecard from entering their lowest-power states, producing a higher and misleading baseline reading.
-
-Use the Mojo to measure whole-device energy over a full 24-hour cycle on your specific hardware; actual consumption depends on signal conditions, LiPo charge efficiency, and the DC-DC module's light-load efficiency.
-
-**Trace anomalies to diagnose:**
-- **Continuous 80–150 mA baseline:** the host is not sleeping — `card.attn` is not cutting Cygnet power. Confirm you are using a Notecarrier CX (which supports ATTN host gating) and that `NotePayloadSaveAndSleep` is executing rather than returning early.
-- **Hourly bursts exceeding 60 seconds:** the radio is struggling on weak signal. Move the cellular antenna or improve antenna routing out of the cabinet.
-- **No blips, no bursts:** the Notecard is not reaching Notehub. Check `PRODUCT_UID` matches the Notehub project exactly and verify cellular coverage at the installation site.
-
-Mojo is not required in deployed hardware; it is a bench bring-up and regression tool. Once a firmware version passes the trace check, deployed units don't need it.
-
-
-A Notecarrier CX and a Cell+WiFi Notecard, paired with a 20-bit precision current monitor and a surface thermistor, turn a passive backup battery into a continuously-monitored asset that reports float conditions every two minutes, tracks state-of-charge via coulomb counting, trends float current over months as an early indicator of VRLA capacity degradation, and pages an operator within one sample interval of the site going dark. The same hardware and firmware run on 12 V roadside cabinet batteries and 24 V industrial UPS banks. The cellular uplink is independent of every piece of equipment the battery protects — which is the whole point. When the cabinet's own LTE radio shuts down because the mains failed and the battery turned out to be half-sulfated and incapable of holding the load, the sentinel is still running — riding on that failing battery through the DC-DC converter, capturing the discharge curve as the voltage collapses, and then switching to the onboard LiPo for the final reporting tail as the bus drops out.
-
-For operations teams maintaining a fleet of roadside or remote-cabinet batteries, the float-current trend in `battery_summary.qo` is the early-warning signal that pays for the deployment many times over: elevated float current weeks before a battery fails a load test is the difference between a scheduled swap during a maintenance window and an emergency truck roll at 2 AM. Commission `usable_capacity_ah` and `soc_pct_init` at installation to unlock `soc_pct` trending alongside float current — two complementary leading indicators. Route `battery_alert.qo` into your existing CMMS and `battery_summary.qo` into a time-series database, and within a service season you will have a corpus of leading indicators to compare against actual failures — the foundation of a proactive battery-management program built on real field data.
-
-
-## 10. Troubleshooting
-
-Most bring-up failures fall into a small set of categories — wiring polarity on the INA228 shunt, a mismatched `PRODUCT_UID`, marginal cellular coverage inside a metal cabinet, or a misconfigured threshold producing alerts that look like sensor faults. Work the table below top-to-bottom: confirm the Notecard is reaching Notehub before chasing sensor symptoms, then validate the INA228 reads, then evaluate alert behavior.
-
-| Symptom | Likely Cause | What to Check |
-|---|---|---|
-| Device never appears on the Notehub **Devices** tab; no `_session.qo` events. | `PRODUCT_UID` is empty or does not exactly match the Notehub project; cellular antenna is inside a sealed metal cabinet; SIM not yet activated on its first session. | Re-verify `PRODUCT_UID` in `cabinet_battery_sentinel_helpers.h` matches the Notehub project. Move the test unit outdoors or attach a known-good external antenna. Confirm `{"req":"card.status"}` in the [In-Browser Terminal](https://dev.blues.io/terminal/) reports a connected modem. Allow up to 5 minutes on a cold start. |
-| `_session.qo` arrives but `battery_summary.qo` never does. | First `summary_interval_min` window has not elapsed yet; INA228 init is failing every wake so all samples are rejected; firmware is not running. | Watch the serial monitor at 115200 baud. Each wake should print `[sentinel] volt=…` and `[sentinel] temp=…`. If you see only `INA228 FAIL`, the chip is not responding — see the next row. |
-| `[sentinel] INA228 FAIL` on every wake, or `ina228_unreachable` alert fires repeatedly. | Qwiic cable not seated, INA228 not powered, or I2C address-select jumpers conflicting with the firmware default (0x40). | Reseat the Qwiic cable on both ends. Confirm the breakout's onboard 3.3 V LED is lit. Check the A0/A1 jumpers on the INA228 breakout — leave them at the factory default unless you have changed `INA228_I2C_ADDRESS` in firmware. |
-| `volt_v` reads `0`, a flat clamped value, or wildly negative numbers. | Battery (+) is not connected to the INA228 `V+` pad, the load bus is mis-wired to ground rather than to `V-`, or the bus voltage exceeds the INA228 common-mode limit (85 V). | Probe `V+` to circuit ground with a DMM and confirm it sits at the battery's terminal voltage. Verify `V-` is bonded to the load bus, not to the battery negative. Confirm the bus is within 12 V or 24 V VRLA/LFP range as covered by §4. |
-| `curr_ma` is stuck at `0.0` regardless of charger state. | Shunt is shorted across by a parallel conductor (the load bus is bonded directly to the battery positive instead of routing through the INA228), or the shunt-resistance constant in firmware does not match the installed shunt. | Remove parallel paths so the only conductor between battery (+) and the load bus passes through the INA228. Confirm `INA228_SHUNT_OHMS` matches the breakout's onboard shunt (15 mΩ) or the external shunt you installed. |
-| Frequent `power_outage` alerts on a site that has stable mains power. | Shunt wiring is reversed (battery on `V-`, load on `V+`), so charge current reads as negative; or `discharge_ma` threshold is too close to zero and ordinary float-current noise crosses it. | Verify shunt polarity per §5: battery (+) to `V+`, load bus (+) to `V-`. The current sign convention is documented in §7.3 — positive means charger present, negative means discharging. If polarity is correct, raise the magnitude of `discharge_ma` (for example to `-500`) via Notehub environment variable. |
-| `temp_c` reads `−9999.0` while voltage and current readings look fine. | NTC thermistor probe is open, shorted, or the 10 kΩ divider resistor is missing. | Probe A0 with a DMM at room temperature — it should sit near 1.65 V with a 10 kΩ NTC and a 10 kΩ pull-up to 3V3. Inspect the divider wiring against §5. |
-| `battery_summary.qo` fields all carry `−9999` and `samples` is `0`. | INA228 was unreachable for the entire window; the sentinel-filled summary preserves time-series continuity. | Treat this as a hard sensor fault — see the INA228 row above. The simultaneous `ina228_unreachable` alert in `battery_alert.qo` is the immediate notification. |
-| Cellular session takes more than 60 seconds, or hourly bursts run for several minutes on the Mojo trace. | Marginal LTE Cat-1 bis signal inside the cabinet; rubber-duck or internal antenna in a metal enclosure. | Reroute the antenna to the cabinet exterior using the SMA bulkhead pigtail described in §4. Re-test with the antenna in clear sky view. |
-| The sentinel itself runs flat after a few hours on the bench. | USB power is connected at the same time as the +VBAT header, preventing the Cygnet from entering its lowest-power state, or the LiPo is undersized for the chosen sample interval. | Disconnect the USB cable for the baseline measurement (see §9). Confirm the 2000 mAh LiPo is plugged into the JST connector. If the sample interval has been pushed below 30 seconds, expect proportionally higher average current. |
-
-If a symptom does not appear above, post a description (with the relevant `[sentinel]` serial output and the trailing `_session.qo` payload) on the [Blues community forum](https://discuss.blues.com).
-
-
-## 11. Limitations and Next Steps
-
-This design targets one pack on one bus and the pack-level signal that catches the most common cabinet failure modes. Cell-level diagnostics, multi-bus shelters, and full state-of-health modeling are deliberate scope choices left to companion designs — the goal here is to get the simplest defensible cabinet sentinel into the field, then layer on richer telemetry where the site warrants it.
-
-### Simplified for the POC
-
-The simplifications below are deliberate scope choices — each is a place where a production deployment will reach for another channel, another sensor, or a richer state estimator once a specific site demands it.
-
-**Single pack, single bus.** The firmware monitors one pack on one positive-referenced DC bus. Cabinets with a primary bank and a parallel reserve, or multi-bus telecom shelters with separate −48 V plant and 12 V auxiliary loads, need one sentinel per bus or a multi-channel front end.
-
-**One INA228 with a single shunt.** The default 15 mΩ onboard shunt covers up to roughly 8 A continuous. Higher-current installations require an external busbar shunt (see §4 and §5); the firmware exposes `INA228_SHUNT_OHMS` and `INA228_MAX_CURRENT_A` for that case but does not auto-detect.
-
-**Pack-level signals only — no cell-level visibility.** Per-cell voltage and per-cell imbalance detection are out of scope. Float-current trending identifies that the string as a whole is degrading, not which cell is failing.
-
-**No state-of-health (SoH) modeling.** SoC is tracked by coulomb counting against a commissioned `usable_capacity_ah`, but the firmware does not estimate capacity fade over time. Re-commissioning `usable_capacity_ah` after a measured discharge cycle is a manual step.
-
-**No temperature compensation on voltage thresholds.** VRLA float voltage drifts with battery temperature (typically −3 to −5 mV per cell per °C). The fixed `volt_min_v` and `volt_max_v` thresholds do not adjust automatically; in a cabinet that swings 30 °C across seasons, set thresholds with margin or split the fleet by climate zone.
-
-**One thermistor on the case surface.** A single surface probe near the geometric center of the pack flags gross thermal events but cannot distinguish a hot cell from charger-induced bulk heating, and it cannot resolve internal cell temperature.
-
-**OCV-based SoC is approximate, especially under load.** Coulomb counting against the nameplate capacity is the only state estimator; the firmware does not blend in OCV at rest, does not detect a full-charge event automatically, and will drift between manual recalibrations.
-
-**No inbound command channel.** Operators tune behavior via Notehub environment variables — there is no `battery_command.qi` for fleet-triggered actions such as "clear alert cooldowns" or "reset SoC after a pack swap."
-
-### Production Next Steps
-
-Once a real cabinet fleet is running the basic sentinel, the following extensions are the natural progression — roughly from the most immediately useful to the most integration-dependent.
-
-**Add a temperature probe on the pack interior or terminal post** for a more accurate read of the chemistry's thermal state, and apply temperature compensation to the float-voltage thresholds and to the coulomb-counting charge efficiency factor.
-
-**Blend OCV at rest with coulomb counting** for a self-correcting SoC estimate that recovers from drift after a long quiescent period, and add an automatic full-charge detector to re-anchor SoC to 100 percent without operator intervention.
-
-**Layer in cell-level monitoring** via a dedicated cell-monitor IC (TI BQ76940 or equivalent), CAN, or Bluetooth into a pack-resident BMS. A cell-level channel transforms the float-current signal from a string-level aggregate into a per-cell diagnostic.
-
-**Integrate with the site's existing BMS** when one is present: many telecom-grade VRLA strings ship with a CAN or RS-485 BMS that already exposes per-cell voltages and SoH. Adding a CAN front end and a vendor-specific frame parser turns this design into a redundant cellular uplink for that BMS rather than a parallel measurement.
-
-**Aggregate across a fleet** in a downstream time-series store. Float-current trends per cabinet, ranked against the fleet median, surface the outlier cabinets weeks before any one of them trips an absolute threshold.
-
-**Deploy [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** so threshold recipes, OCV blends, and chemistry-specific calibrations can be pushed across the fleet without a truck roll.
-
-**Add an inbound `battery_command.qi` Notefile** for fleet operators to reset SoH accumulators after a pack swap, clear alert cooldowns following a planned maintenance visit, or trigger an immediate ad-hoc sample-and-report.
-
-
-## 12. Summary
-
-The traffic engineer (or telecom site owner, or industrial RTU operator) now has the one signal that matters: a creeping float-current trend weeks before a battery would have failed under load, and an immediate page within one sample interval if the site does go dark. Because the Notecard rides on its own cellular session, the "battery is failing" alert reaches Notehub even when the site's own modem and gateway have already lost power — and the LiPo reporting tail captures the most diagnostically valuable minutes of a battery's life: the moments after mains drops and before the bus collapses. One avoided emergency truck roll, or one intersection that stays up through a regional outage, pays for the sentinel many times over.
-
-The same architecture and firmware — with the threshold recipe and shunt sized per chemistry and per current range — carries from a 12 V VRLA roadside cabinet to a 24 V LFP industrial UPS bank, and from one cabinet to a fleet once `battery_summary.qo` is routed into a time-series store and ranked against the fleet baseline.
+A cellular reference design for backup batteries in traffic-signal controllers and industrial RTUs that continuously monitors voltage, current, temperature, and state of charge with proactive alerts.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/remote-cabinet-backup-battery-sentinel/)**
diff --git a/84-remote-cabinet-backup-battery-sentinel/banner.png b/84-remote-cabinet-backup-battery-sentinel/banner.png
deleted file mode 100644
index c78efae7..00000000
Binary files a/84-remote-cabinet-backup-battery-sentinel/banner.png and /dev/null differ
diff --git a/84-remote-cabinet-backup-battery-sentinel/diagrams/01-system-architecture.svg b/84-remote-cabinet-backup-battery-sentinel/diagrams/01-system-architecture.svg
deleted file mode 100644
index 2bb4f344..00000000
--- a/84-remote-cabinet-backup-battery-sentinel/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,79 +0,0 @@
-
diff --git a/84-remote-cabinet-backup-battery-sentinel/diagrams/02-wiring-assembly.svg b/84-remote-cabinet-backup-battery-sentinel/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 152403ef..00000000
--- a/84-remote-cabinet-backup-battery-sentinel/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,118 +0,0 @@
-
diff --git a/84-remote-cabinet-backup-battery-sentinel/diagrams/03-data-flow.svg b/84-remote-cabinet-backup-battery-sentinel/diagrams/03-data-flow.svg
deleted file mode 100644
index 4ff82b77..00000000
--- a/84-remote-cabinet-backup-battery-sentinel/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,84 +0,0 @@
-
diff --git a/85-cellular-medication-adherence-pillbox/README.md b/85-cellular-medication-adherence-pillbox/README.md
index 7762adc9..958fc34e 100644
--- a/85-cellular-medication-adherence-pillbox/README.md
+++ b/85-cellular-medication-adherence-pillbox/README.md
@@ -1,465 +1,5 @@
# Cellular Medication Adherence Pillbox
-
-
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Cellular%20Medication%20Adherence%20Pillbox).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/cellular_medication_adherence_pillbox/](firmware/cellular_medication_adherence_pillbox/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [remote patient monitoring](https://blues.com/remote-patient-monitoring/) device that catches missed doses before they become clinical events. A Blues Notecard Cell+WiFi riding on a Notecarrier CX wakes every 30 seconds, reads seven snap-action micro-switches inside a standard weekly pillbox, and uploads a cellular event to the [Blues Notehub](https://blues.com/notehub/) cloud service **each time a compartment lid is detected open during a scheduled 30-second poll**: no WiFi configuration, no smartphone, and nothing for the patient to set up.
-
-
-## 1. Project Overview
-
-**The problem.** Medication non-adherence is among the most consequential and under-reported drivers of poor outcomes in chronic disease. Patients with diabetes, heart failure, hypertension, or transplant histories who skip or mistime doses cost the healthcare system tens of billions of dollars annually in preventable hospitalizations, and far more in years of life. A clinician or care coordinator who knows a patient hasn't opened their Tuesday compartment by 10 PM can intervene with a phone call; one who only discovers the missed doses at the next clinic visit, six weeks later, is always playing catch-up.
-
-Existing IoT pillboxes have tried to close this gap, and most fail in the same place: the connectivity model. WiFi-dependent devices work fine in young, tech-fluent households. They fail quietly in the homes of elderly patients — who are statistically the highest-risk population — because those homes have locked WiFi routers, forgotten passwords, carrier-grade NAT, and no one around to troubleshoot a dropped connection at 7 AM. Bluetooth-to-smartphone bridges work until the phone is low on battery, out of range, or the patient ignores the pairing prompt. The result is a connected pillbox that isn't connected.
-
-**Why cellular-first.** The Notecard Cell+WiFi changes the deployment calculus. Cellular connectivity is already provisioned on the device — no router credentials, no smartphone, no IT ticket. The box works the moment it is placed on the patient's nightstand. WiFi remains available as an optional fallback at sites that can offer a nearby access point, but it is never a dependency. For the actual target demographic of chronic-disease patients — many of whom are elderly, live alone, or lack consistent technical support — this is the design decision that makes the device credible as a monitoring tool rather than a gadget. An **RPM** (remote patient monitoring) device that requires patient-side network administration isn't really a remote monitoring device; it's homework.
-
-
-
-**Deployment scenario.** The device ships pre-provisioned: the Notecard's ProductUID is flashed at the pharmacy or care coordinator's office, and the patient only needs to plug in a USB charger (or replace the LiPo annually). The clinician views adherence events in Notehub and receives real-time alerts through a downstream route. No app installation, no WiFi onboarding, and no ongoing patient engagement with the device is required.
-
-**An important signal limitation.** The device reports compartment lid opens, not confirmed ingestion, and weekly tray-loading sessions by a caregiver or pharmacist produce events that are operationally indistinguishable from patient dose-taking opens. A full refill of the seven-day tray can set all seven bits in `daily_opens` in a single polling interval. Downstream workflow must account for this; see [§10](#10-limitations-and-next-steps) for operational mitigations.
-
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** On the patient's nightstand the box looks asleep, and almost always is. Every 30 seconds the Cygnet STM32 host on the Notecarrier CX wakes for a fraction of a second, reads seven compartment GPIO pins, and compares them to the snapshot from the previous wake. A rising edge — a lid that was closed and is now open — turns into a `pill_open.qo` [Note](https://dev.blues.io/api-reference/glossary/#note) marked `sync:true`, telling the Notecard to push it out the door right now instead of holding it for the next scheduled outbound window. The previous pin values, the daily bitmask, and the current UTC day all survive each sleep cycle through `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep`, so no external EEPROM or flash chip is needed.
-
-**Notecard responsibilities.** The Notecard Cell+WiFi (NOTE-MBGLW) handles everything network-related the patient should never have to think about. It queues Notes locally, manages its cellular (or opportunistic WiFi) session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `periodic` outbound cadence, and flushes any `sync:true` Note immediately the moment it sees one. On the inbound side it pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) down from Notehub, so a care coordinator can retune the poll interval, the daily summary hour, or either sync cadence without sending new firmware. Between sessions it idles at roughly 8–18 µA, effectively invisible on the LiPo budget.
-
-**Notehub responsibilities.** Every event that leaves the box lands in [Notehub](https://notehub.io), which ingests, stores, and applies project routes. `pill_open.qo` events arrive in near-real time for the clinician's queue; `pill_summary.qo` Notes accumulate for adherence trends and reporting. [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices by care coordinator, practice, or patient tier, so per-fleet environment variables apply across a whole cohort at once.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-
-## 3. Technical Summary
-
-1. **Notehub:** create a [Notehub project](https://notehub.io), copy its ProductUID.
-2. **Wire the bench rig:** Notecarrier CX + Notecard MBGLW + 7 snap-action micro-switches on D5, D6, D9–D13. Full pinout in [§5](#5-wiring-and-assembly).
-3. **Edit one line** of [`firmware/cellular_medication_adherence_pillbox/cellular_medication_adherence_pillbox.ino`](firmware/cellular_medication_adherence_pillbox/cellular_medication_adherence_pillbox.ino): search for `#define PRODUCT_UID` and set it to your project's value.
-4. **Flash:** select the Cygnet board in the Arduino IDE, hit Upload. Full instructions in [§7.1](#71-installing-and-flashing).
-5. **Watch** — open Notehub → your project → **Events** tab. You should see a `_session.qo` within a minute and a `pill_open.qo` each time you open a compartment.
-
-
-Here is a sample Note this device emits:
-
-```json
-{
- "compartment": 4,
- "label": "WED",
- "day_opens_mask": 15,
- "opened_this_poll": 8
-}
-```
-
-## 4. Hardware Requirements
-
-**Compartment sensing — snap-action micro-switches.** This design uses subminiature snap-action micro-switches for compartment lid detection. Each switch mounts in the pillbox base with its roller lever positioned to engage the closed lid; when the lid opens the lever releases and the circuit opens. Micro-switches produce a clean mechanical signal that works directly with the Cygnet's internal pull-up resistor, require no magnets, bias supply, or per-compartment conditioning resistors, and tolerate the tens of thousands of actuation cycles typical of a multi-year patient deployment. The [Adafruit #819](https://www.adafruit.com/product/819) roller-lever micro-switch is a convenient off-the-shelf choice, with its SPDT body and pre-installed roller lever making lid-contact alignment straightforward on a variety of pill tray geometries.
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32L4 host; no separate MCU needed. Exposes 7 digital I/O pins (D5, D6, D9–D13) exactly matching the 7-day compartment count. |
-| [Notecard Cell+WiFi (NOTE-MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Cellular with WiFi fallback. See the [NOTE-MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) for radio and power specifications. Cellular removes all patient-side network configuration; cellular-first is the deployment model that serves the target demographic. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter for bench-validation of sleep/wake power budget before patient deployment. Not required in production. See [§9](#9-validation-and-testing). |
-| [Adafruit #819 Micro Switch w/Roller Lever](https://www.adafruit.com/product/819), ×7 | 7 | One switch per compartment lid. Mounts in the pillbox base with the roller lever engaging the closed lid. Wire C to the digital pin and NO to GND; leave NC unconnected. The Cygnet's internal pull-up holds the pin HIGH when the lid is open; the closed lid depresses the lever and pulls the pin LOW. |
-| Cellular and WiFi u.FL antenna leads (included with NOTE-MBGLW) | 1 set | Both the cellular and WiFi u.FL flex-antenna leads ship inside the NOTE-MBGLW retail box; no separate antenna purchase is required for an indoor pillbox installation. Attach each lead to the matching u.FL connector on the Notecard face and route flat inside the enclosure away from the LiPo. See §4. |
-| [Adafruit #328 Lithium Ion Polymer Battery](https://www.adafruit.com/product/328), 3.7 V 2500 mAh, JST-PH connector | 1 | Direct plug-in to the Notecarrier CX LIPO JST connector. At the expected power budget (~5–15 mAh/day), a 2500 mAh cell provides multi-month autonomy between charges. |
-| [EZY DOSE Contoured Weekly Pill Planner](https://shop.apothecaryproducts.com/products/ezy-dose-contoured-weekly-pill-planner) (Apothecary Products, part #67790) | 1 | Patient-facing medication organizer: seven compartments with clear lids, one per day of the week. Available at pharmacies and medical supply retailers under the EZY DOSE brand. |
-| [Hammond 1591SSBK ABS project enclosure](https://www.hammfg.com/part/1591SSBK), 110 × 82 × 44 mm | 1 | Electronics housing for the Notecarrier CX and LiPo. Mounts as a sidecar alongside or beneath the pill organizer; only the micro-switch wiring leads penetrate the pill tray body. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service: no monthly commitment.
-
-
-## 5. Wiring and Assembly
-
-
-
-The Notecard Cell+WiFi (NOTE-MBGLW) seats into the Notecarrier CX's M.2 connector and is powered from the same VBAT rail. All host I/O lands on the [Notecarrier CX dual 16-pin header](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/).
-
-**Micro-switch wiring — all 7 compartments are wired identically:**
-
-Each micro-switch is an SPDT device with three terminals: Common (C), Normally Open (NO), and Normally Closed (NC). Connect C to the assigned digital pin and NO to any GND pin on the header; leave NC unconnected. Mount the switch so the closed lid depresses the roller lever, connecting C to NO and pulling the pin LOW. When the lid opens, the lever releases, the C–NO circuit opens, and the Cygnet's internal pull-up holds the pin HIGH. No external resistors are needed.
-
-| Compartment | Day Label | Notecarrier CX Pin |
-|---|---|---|
-| 1 | SUN | D5 |
-| 2 | MON | D6 |
-| 3 | TUE | D9 |
-| 4 | WED | D10 |
-| 5 | THU | D11 |
-| 6 | FRI | D12 |
-| 7 | SAT | D13 |
-
-Pin-by-pin:
-
-- **D5, D6, D9, D10, D11, D12, D13** (the seven available GPIOs) → micro-switch C (Common) terminal of each switch. Note that D7 and D8 are not present on the Notecarrier CX header; the usable GPIO range is D5, D6, then D9–D13.
-- **GND** (any GND pin on the header) → micro-switch NO (Normally Open) terminal of each switch.
-- **NC terminal** of each switch → leave unconnected.
-- No external resistors required — all pull-ups are internal to the Cygnet STM32L4.
-
-**Switch and actuator placement:**
-
-Mount each switch flush in the pill tray base directly beneath the path of the lid, with the roller lever pointing upward toward the lid's travel path. Position the switch so the lid rim or a small actuator nub depresses the roller 0.5–1 mm when the lid is fully closed — enough to produce a clean snap-action transition but not so much that the lid cannot close completely. With the lid fully closed the pin should read ~0 V (LOW); with the lid open it should read ~3.3 V (HIGH from the internal pull-up). Verify with a multimeter or the serial monitor before final assembly.
-
-**LiPo battery:**
-
-Connect the LiPo's JST-PH plug directly into the Notecarrier CX's onboard LIPO connector. For bench bring-up with Mojo, splice the Mojo inline between the LiPo and the +VBAT header pin instead of using the LIPO JST connector; this lets Mojo measure the entire current draw of the Notecarrier, Notecard, and sensors together.
-
-**Antennas:**
-
-Attach the cellular and WiFi u.FL antenna leads — both ship in the NOTE-MBGLW retail box (see BOM) — to the corresponding u.FL connectors on the Notecard face. No separate antenna purchase is required. Position the leads flat inside the enclosure away from the LiPo cell.
-
-**Physical assembly — enclosure and switch mounting:**
-
-Mount the Hammond 1591SSBK enclosure as a sidecar alongside or beneath the pill organizer tray using two-sided foam tape or a 3D-printed bracket; the enclosure and tray travel together as a unit with only the micro-switch wiring leads crossing the enclosure wall.
-
-Drill or punch seven 4–5 mm cable pass-through holes in the enclosure wall facing the pill tray — one per compartment lead pair. Thread each pair through its own hole with a rubber grommet, or apply a bead of hot-melt adhesive around the wire as a strain-relief bead before the lead enters the enclosure. Strain relief prevents the header-pin connection from pulling out if a lead is tugged from the pill-tray side.
-
-Mount each micro-switch body inside the tray base using the switch's mounting holes with two M2 self-tapping screws, or secure it with an adhesive patch or a 3D-printed bracket. Position the switch so the roller lever aligns with the inner face of the lid rim — the lever should be depressed 0.5–1 mm when the lid is fully closed and return freely to its neutral position when the lid is open. Verify with a multimeter before final assembly: the pin should read LOW (≈0 V) with the lid closed and HIGH (≈3.3 V) with the lid open.
-
-Retain the LiPo inside the enclosure with a strip of hook-and-loop tape (Velcro) or a foam-padded retaining clip so the JST connector is not under mechanical strain. Orient the battery's flat face against the enclosure base and route the lead so it cannot be pinched when the enclosure lid is closed.
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid); it looks like `com.your-company.your-name:pillbox`.
-
-2. **Set the ProductUID in firmware.** Open `cellular_medication_adherence_pillbox.ino` and replace the empty string on the `#define PRODUCT_UID ""` line with your value.
-
-3. **Claim the Notecard.** Power the assembled unit. On first cellular connection the Notecard associates itself with your Notehub project automatically; no manual claim step required. The device will appear in your project's **Devices** tab within a minute or two.
-
-4. **Create a Fleet per care group.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) and [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) are how Notehub groups devices for shared configuration. A natural grouping is one fleet per care coordinator or prescribing physician, so that environment variables (alert windows, sync cadence) can be tuned once and applied to all their patients' devices simultaneously.
-
-
-
- **Privacy Note.** For an RPM deployment, patient identifiers should never appear in Note body payloads; they travel in plaintext through any downstream route and may be stored in third-party systems. They also should not be placed in Notehub device metadata (tags, serial-number field): Note bodies and device metadata are not designed as stores for protected health information, and any PHI placed there would be outside a controlled covered system. Instead, assign only an opaque, non-PHI deployment identifier (for example, a random UUID or a clinic-assigned device code) to the [Notehub device tag](https://dev.blues.io/notehub/notehub-walkthrough/#organizing-devices-by-tag) or serial-number field, and maintain the patient-to-device mapping exclusively in your downstream covered system, keyed on the Notecard's device UID. If your organization's downstream architecture requires routing PHI through any cloud component, review Blues' contractual and compliance posture with qualified counsel before proceeding.
-
-
-
-5. **Set environment variables.** Navigate to **Fleet → Environment** in Notehub. All variables below are optional; firmware defaults apply if omitted. The device pulls updates on its next inbound sync (default every 2 hours) — no reflash required.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `poll_interval_sec` | `30` | Seconds between host wakes for compartment polling. Lower values reduce open-detection latency; minimum enforced at 15. |
- | `summary_hour_utc` | `0` | UTC hour (0–23) on the day following each UTC day's end at or after which the previous day's adherence summary is queued for transmission. `0` means the summary is queued as soon as midnight UTC passes; set to match the care coordinator's morning review time (e.g., `7` for 7 AM UTC). |
- | `outbound_min` | `720` | Minutes between Notecard outbound syncs. Immediate open events are never delayed by this value; they use `sync:true`. Changing this value triggers an automatic `hub.set` reapply. |
- | `inbound_min` | `120` | Minutes between Notecard inbound syncs (environment variable fetch cadence). Changing this value triggers an automatic `hub.set` reapply. |
-
-6. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `pill_open.qo` (to a real-time alert endpoint, SMS, email, nurse-call system, or care-coordination platform) and a second for `pill_summary.qo` (to a long-term analytics store or dashboard). Separating the two Notefiles at the source means the real-time alert path has no dependency on the analytics path.
-
-### What you should see in Notehub
-
-Within a minute of first power-on, the **Events** tab should start populating. Three event types matter for this project:
-
-- **`_session.qo`** — automatic Notecard housekeeping on each cellular session. The presence of these events is the fastest way to confirm the radio is reaching Notehub. If you see no `_session.qo` events in the first 2–3 minutes, check that `PRODUCT_UID` matches your Notehub project exactly and that cellular coverage is available.
-- **`pill_open.qo`** — one Note per detected lid-open event. Detection latency is up to 30 seconds (next poll wake); Notecard cellular session establishment then adds roughly 15–60 seconds, for a typical end-to-end window of under two minutes from physical open to Notehub receipt. For this to be reliable, the lid must remain open (or be re-opened) until the next poll fires — a lid opened and re-closed within the same 30-second interval is not detected. Multiple opens of the same compartment lid in one day each produce a separate Note. The body looks like:
- ```json
- {
- "compartment": 4,
- "label": "WED",
- "day_opens_mask": 15,
- "opened_this_poll": 8
- }
- ```
- `day_opens_mask` is a 7-bit bitmask of every compartment opened so far today (including this one), so each event body is self-contained and the full picture can be reconstructed from a single Note. Bit positions map directly to compartment numbers (bit 0 = compartment 1 = SUN, bit 6 = compartment 7 = SAT). `opened_this_poll` is the bitmask of compartments detected open in this specific 30-second polling wake — when multiple bits are set, it indicates that several compartments were opened simultaneously (or at least within the same polling interval), which is a strong signal of a weekly tray-refill session rather than a routine single-dose open. Downstream route logic can threshold on the bit-count of `opened_this_poll` to distinguish refill clusters from individual dose events; see [§10](#10-limitations-and-next-steps) for the full discussion.
-
-- **`pill_summary.qo`** — one Note per UTC day, queued at the `summary_hour_utc` threshold (default midnight UTC) on the day following each data-collection period and then delivered on the next Notecard sync session — either the scheduled outbound sync or any earlier sync triggered by a `pill_open.qo` event. The body looks like:
- ```json
- {
- "opens_mask": 31,
- "opens_count": 5
- }
- ```
- `opens_mask` uses the same bitmask convention as `pill_open.qo`. Bit 0 = Sunday (compartment 1), bit 6 = Saturday (compartment 7). `opens_count = 0` means no compartments were opened that day — the patient missed all doses — which the `full:true` flag on `note.add` ensures is preserved through the template's omitempty suppression.
-
-- **`pill_diag.qo`** — an exceptional diagnostic Note emitted on **pending-event queue overflow**: `error: "pending_overflow"` and a `dropped` count when the 28-entry retry ring buffer fills and the oldest queued open event is evicted. A matching `error: "pending_overflow_cleared"` Note — with the total `dropped` count for the episode — is emitted when the queue subsequently drains. These Notes appear in both bench and production modes and indicate that adherence data was lost while the Notecard was unable to accept `note.add` requests. Route `pill_diag.qo` alongside `pill_open.qo` if data-integrity alerting is required.
-
- An ATTN→EN power-gating fault (host MCU never loses power after `NotePayloadSaveAndSleep`) is **not** reported through this Notefile because any `note.add` issued in that race window is dispatched while the Notecard is already entering sleep mode and is unreliable. Detect this fault instead via the absence of the expected `_session.qo` cadence in Notehub and the bench-mode USB serial output noted in [§7.1](#71-installing-and-flashing).
-
-
-## 7. Firmware Design
-
-The firmware is split across a main sketch and two helper files, all three living in [`firmware/cellular_medication_adherence_pillbox/`](firmware/cellular_medication_adherence_pillbox/):
-
-- **[`cellular_medication_adherence_pillbox.ino`](firmware/cellular_medication_adherence_pillbox/cellular_medication_adherence_pillbox.ino):** `setup()` entry point and main application logic.
-- **`cellular_medication_adherence_pillbox_helpers.h`:** shared constants, struct definitions, and helper function declarations.
-- **`cellular_medication_adherence_pillbox_helpers.cpp`:** helper function implementations (sensor reading, Notecard configuration, event emission, state management).
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Install via the Arduino Boards Manager by adding the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**, then search for "STM32 MCU based boards" and install. Select **Blues Cygnet** as the board target (canonical FQBN: `STMicroelectronics:stm32:Blues:pnum=CYGNET`).
-- **`Blues Wireless Notecard`** library (`note-arduino`). Install via the Arduino Library Manager (search "Blues Wireless Notecard") or run `arduino-cli lib install "Blues Wireless Notecard"`. See [note-arduino releases](https://github.com/blues/note-arduino/releases) for changelog and any newer stable versions.
-
-**Flashing — Arduino IDE:** open `cellular_medication_adherence_pillbox.ino`, select the Cygnet board, hit **Upload**. The Notecarrier CX exposes the ST-Link interface on the USB cable — no external programmer needed.
-
-**Flashing — `arduino-cli`:** from the firmware directory,
-```bash
-# Confirm the FQBN for your installed core version (the Cygnet variant lives
-# under the "Blues boards" group, not under "Generic STM32L4 series").
-arduino-cli board details -b STMicroelectronics:stm32:Blues | grep -i cygnet
-
-# Find the USB port the Notecarrier enumerates as (typically /dev/cu.usbmodem*
-# on macOS, COMx on Windows, or /dev/ttyACM* on Linux). The ST-Link shows up as
-# "STMicroelectronics STM32 STLink" in your system device list.
-# On macOS: ls -la /dev/cu.* | grep usb
-# On Linux: ls -la /dev/ttyACM*
-# On Windows: check Device Manager > Ports.
-
-# Then compile + upload (replace the FQBN and port below with your values).
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET cellular_medication_adherence_pillbox.ino
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem14201 cellular_medication_adherence_pillbox.ino
-```
-
-After upload, open the serial monitor at **115200 baud**. On first boot you should see `[init] Notecard configured` and `[init] Templates defined`, then the device goes silent as it enters its first 30-second sleep cycle. Opening a compartment lid on the next wake prints `[open] compartment=N (DAY) day_mask=0bXXXXXXX`.
-
-**Bench mode vs. production mode.** `NotePayloadSaveAndSleep()` always returns to the host once it has dispatched the `card.attn` sleep command — it is the Notecard's subsequent ATTN de-assertion that actually cuts host power on a correctly wired carrier. The helper header ships with `PILLBOX_BENCH_MODE` commented out (production default). When this define is uncommented in `cellular_medication_adherence_pillbox_helpers.h`, `sleepHost()` falls back to `delay()` + `NVIC_SystemReset()` — useful for bring-up on configurations where the Notecard ATTN pin is not wired to the host EN rail. **Before deploying to a battery-powered Notecarrier CX, confirm that `PILLBOX_BENCH_MODE` remains commented out.** In production mode, if the host is still alive after the sleep command was dispatched (an ATTN→EN wiring fault), the firmware logs over USB serial if available and halts, preventing silent battery drain. The fault surfaces in Notehub as a missing `_session.qo` cadence; it is not signalled by a `pill_diag.qo` Note because any `note.add` request issued in the post-sleep race window is dispatched while the Notecard is already entering sleep mode and would not be reliably delivered.
-
-### 7.1a Bench mode critical Note
-
-Before deploying to a battery-powered Notecarrier CX, **verify that `PILLBOX_BENCH_MODE` remains commented out** in `cellular_medication_adherence_pillbox_helpers.h`. In production mode, the Notecard ATTN pin gates the host's 3.3V rail, cutting power between polling wakes. If ATTN→EN is miswired or disconnected, the host stays alive and drains the LiPo continuously. The firmware will halt and log `[FATAL] NotePayloadSaveAndSleep returned and host did not lose power` over USB serial, preventing silent battery drain. You'll detect this in Notehub as a missing `_session.qo` cadence; never ignore consecutive gaps in session events — it signals a power-gating fault that must be corrected before patient deployment.
-
-### 7.2 Modules
-
-| Responsibility | Function |
-|---|---|
-| Notecard configuration (`hub.set`, accelerometer disable) | `initNotecard()` |
-| Note template registration for `pill_summary.qo` | `defineTemplates()` |
-| Environment variable fetch and clamp | `fetchEnvOverrides()` |
-| Compartment GPIO pin sampling | `sampleCompartments()` |
-| Immediate open-event emission | `emitOpenEvent()` |
-| Daily adherence summary emission | `emitDailySummary()` |
-| UTC time query for day-rollover detection | `utcDayAndHour()` |
-| State persistence and host sleep | `sleepHost()` |
-
-### 7.3 Sensor reading strategy
-
-On each 30-second wake, `sampleCompartments()` reads all seven digital pins into a single byte bitmask. The firmware XORs this against the `prev_pin_mask` stored in `PillboxState` to compute a `newly_opened` byte — bits set in `newly_opened` represent pins that transitioned from LOW (closed) to HIGH (open) since the last wake. **Every detected rising edge generates a `pill_open.qo` event** — multiple opens of the same compartment lid in a day each produce a separate Note, allowing downstream systems to correlate opening patterns against the patient's dosing schedule. The `daily_opens` bitmask tracks which compartments were opened at all today and feeds the end-of-day summary; the `day_opens_mask` field in each `pill_open.qo` body is a running snapshot of that bitmask at the moment of the event, making each event payload self-contained. A second field, `opened_this_poll`, records which compartments were detected open in this specific wake — when multiple bits are set it is a direct downstream signal that multiple lids were opened simultaneously, consistent with a weekly tray-refill session rather than a single dose open.
-
-**Important polling limitation.** Because the firmware compares pin state at discrete 30-second boundaries, a lid that is opened and fully re-closed within a single 30-second interval between wakes produces no rising edge and generates no event. That open is silently missed and is not counted in `daily_opens`. For the typical adherence use case this is not a concern: a patient opening a compartment to take a pill holds it open for several seconds to several minutes, well beyond the polling resolution. However, any brief mechanical disturbance that resolves before the next wake will go undetected. The minimum configurable poll interval is 15 seconds (`poll_interval_sec` environment variable, firmware-enforced floor). If guaranteed sub-second open detection is required, the design would need interrupt-driven GPIO or a latch-based circuit that captures and holds the open state until the next poll.
-
-The maximum detection latency from "lid physically opened" to "event queued for transmission" is equal to the configured poll interval (default 30 seconds). Real-world latency from event queuing to Notehub receipt adds the Notecard's cellular session-establishment time (typically 15–60 seconds), giving an end-to-end window of typically under two minutes.
-
-### 7.4 Event payload design
-
-`pill_open.qo` is left **untemplated** (free-form JSON). Open events are low-frequency even in the every-open model (a typical patient opens each lid once or twice per day, for a total well under 20 Notes/day), making wire overhead negligible. The untemplated format keeps each event easy to inspect in Notehub without adding unnecessary template management overhead. The `opened_this_poll` field carries the bitmask of all compartments detected open in the same polling wake as this event; when multiple bits are set it provides a downstream signal distinguishing a weekly refill session (multiple lids opened simultaneously) from a routine single-compartment dose open.
-
-`pill_summary.qo` is **templated** with `TUINT8` (1-byte unsigned integer) for both fields. The template registration tells the Notecard to store each daily summary as a 2-byte fixed-length record rather than a variable-length JSON string, keeping the on-device queue compact across a full month of data if cellular connectivity is temporarily unavailable.
-
-```json
-// pill_open.qo — untemplated, sync:true, queued on detected open (30-second poll boundary)
-{
- "compartment": 4,
- "label": "WED",
- "day_opens_mask": 15,
- "opened_this_poll": 8
-}
-
-// pill_summary.qo — templated, queued at summary_hour_utc, delivered on next outbound sync
-{
- "opens_mask": 63,
- "opens_count": 6
-}
-```
-
-The `full:true` flag on the daily summary's `note.add` request preserves `opens_count: 0` even though the Note template would normally suppress zero-valued fields. Knowing that a patient opened zero compartments in a day is exactly the signal a care coordinator needs.
-
-### 7.5 Low-power strategy
-
-The Cygnet STM32L4 host MCU is powered off entirely between wakes, not merely sleeping in a low-power mode, but physically de-energized by the Notecard's ATTN pin gating the Notecarrier CX's 3.3V host rail. This means every 30-second cycle consists of ~100–200 milliseconds of active host execution followed by ~29.8 seconds of zero host draw.
-
-After each sample cycle, `sleepHost()` serializes the `PillboxState` struct into the Notecard's flash memory via `NotePayloadAddSegment` and `NotePayloadSaveAndSleep`, then issues `card.attn` in sleep mode. The Notecard cuts host power for `poll_sec` seconds and then re-asserts ATTN, which re-powers the host rail. From the firmware author's perspective, the sleep call is a single function invocation; from the patient's perspective, the device is simply always on.
-
-The Notecard itself remains powered continuously and idles at approximately 8–18 µA between cellular sessions. Cellular sessions — triggered by `sync:true` open events or the scheduled outbound timer for queued summary Notes — draw several hundred milliamps for the duration of the transmission (typically under 30 seconds) and then return to idle.
-
-### 7.6 Retry and error handling
-
-- The first `hub.set` call on cold boot uses `sendRequestWithRetry(req, 5)` to handle the known cold-boot I2C race condition where the host comes up before the Notecard is ready to receive transactions.
-- `fetchEnvOverrides()` uses `requestAndResponse()` and guards against a NULL response; a failed env fetch leaves the current state values unchanged rather than crashing or zeroing thresholds.
-- If `utcDayAndHour()` returns 0 (Notecard hasn't yet synced to get a valid time), the day-rollover branch is skipped entirely. Any opens that occurred before time-sync remain in `daily_opens` and are associated with the first valid UTC day once time becomes available; they are only moved into `prev_day_opens` at the first actual day rollover, at which point they feed the subsequent end-of-day summary.
-- If `NotePayloadRetrieveAfterSleep()` fails or the segment is missing, the firmware treats the wake as a first boot: re-reads the initial pin state and reconfigures the Notecard. This handles the case where the LiPo died and the Notecard lost its stored payload.
-- **`emitOpenEvent()` failure and retry queue.** When a `note.add` fails after all three attempts, `enqueuePendingEvent()` stores the event (compartment index, day mask, poll mask) in a 28-entry ring buffer persisted inside `PillboxState`. On each subsequent wake, `replayPendingOpenEvents()` retries every buffered event before sampling new opens; successfully replayed records are removed and the queue is compacted. If the queue fills before Notecard connectivity is restored, the oldest entry is evicted and a `pill_diag.qo` Note is immediately sent to Notehub with `error: "pending_overflow"` and a `dropped: 1` count — giving cloud-visible data-loss visibility even while the primary note-add path is degraded. When the queue fully drains, a second `pill_diag.qo` with `error: "pending_overflow_cleared"` and the cumulative drop count closes the episode and confirms how many `pill_open.qo` events are missing. The 28-slot capacity covers four consecutive worst-case 7-compartment wakes; a sustained Notecard failure beyond that window causes adherence event loss. See [§10](#10-limitations-and-next-steps).
-
-### 7.7 Key code snippet 1 — template definition
-
-`TUINT8` is defined in `note-c` as the integer constant `21`, which tells the Notecard to encode each templated field as a 1-byte unsigned integer (0–255). The template registers this encoding with the Notecard's on-device compression layer, so every `pill_summary.qo` Note is stored as a fixed 2-byte binary record rather than variable-length JSON. This compression keeps the on-device queue compact across a full month of data if cellular connectivity is temporarily unavailable. For downstream integrations, the Notecard automatically decompresses these records back to JSON when they reach Notehub.
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "pill_summary.qo");
-JAddNumberToObject(req, "port", 50);
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "opens_mask", TUINT8); // 7-bit bitmask, 0–127
-JAddNumberToObject(body, "opens_count", TUINT8); // 0–7 count
-notecard.sendRequest(req);
-```
-
-### 7.8 Key code snippet 2 — immediate open event
-
-`sync:true` tells the Notecard not to wait for the next outbound window; this Note jumps the queue and the radio wakes immediately.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "pill_open.qo");
-JAddBoolToObject (req, "sync", true); // immediate transmission
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "compartment", 4); // 1–7
-JAddStringToObject(body, "label", "WED");
-JAddNumberToObject(body, "day_opens_mask", 15); // running daily bitmask
-JAddNumberToObject(body, "opened_this_poll", 8); // per-poll multi-open bitmask
-notecard.sendRequest(req);
-```
-
-### 7.9 Key code snippet 3 — sleep with state persistence
-
-`NotePayloadSaveAndSleep` serializes the state struct into Notecard flash and issues `card.attn` to cut host power. The host re-enters `setup()` from cold after `poll_sec` seconds.
-
-```cpp
-NotePayloadDesc payload = {0, 0, 0};
-NotePayloadAddSegment(&payload, STATE_SEG_ID, &state, sizeof(state));
-NotePayloadSaveAndSleep(&payload, state.poll_sec, NULL);
-```
-
-
-## 8. Data Flow
-
-
-
-**Collected.** Every 30 seconds: a 7-bit bitmask of compartment lid states, compared against the previous sample. The firmware does not transmit on every wake, only on a state transition.
-
-**Transmitted.**
-- `pill_open.qo` — one Note per detected lid-open event (lid must be open when the 30-second poll fires), immediate (`sync:true`). Contains the compartment number (1–7), its day label, the running daily bitmask of all compartments opened so far today (`day_opens_mask`), and a per-poll bitmask of all compartments detected open in this specific wake (`opened_this_poll`). Multiple bits set in `opened_this_poll` indicate a likely refill session. Multiple opens of the same compartment each generate a separate Note, provided each open is present at a poll boundary.
-- `pill_summary.qo` — one Note per UTC day, template-encoded. Queued at the configured `summary_hour_utc` on the day following each data-collection period (default: midnight UTC) and then delivered on the next Notecard sync session — either the scheduled outbound sync or any earlier sync triggered by a `pill_open.qo` event. Contains `opens_mask` (which compartments opened) and `opens_count` (how many). Uses `full:true` to preserve a zero count when the patient opened no compartments.
-
-**Routed.** Both Notefiles go to Notehub and from there to whatever downstream the project's routes specify. The two filenames are deliberately separate so `pill_open.qo` can fan out to a real-time alert channel (SMS, pager, care platform webhook) while `pill_summary.qo` goes to a long-term store at a different cadence.
-
-**Alerts trigger on.**
-- Any `pill_open.qo` event — a compartment was opened. Route this to whatever real-time channel the care coordinator uses. Note that weekly tray-refill sessions produce events identical to patient dose-taking opens; see [§10](#10-limitations-and-next-steps) for how to distinguish them operationally.
-- Absence of expected `pill_open.qo` by a configurable time window: the patient hasn't opened their morning compartment. This logic lives in the downstream route or dashboard, not in firmware.
-- `pill_summary.qo` with `opens_count: 0`: the patient missed all doses for the day.
-
-
-## 9. Validation and Testing
-
-**Expected cadence after deployment.** In steady state, a patient who takes medication once per day generates one `pill_open.qo` event per day and one `pill_summary.qo` per day. A correctly behaving device with no lids opened should still send a daily `pill_summary.qo` with `opens_count: 0`.
-
-**Bench functional test.** The firmware fires an open event on a LOW→HIGH rising edge — when the pin transitions from LOW (actuator pressed, switch closed, lid closed) to HIGH (actuator released, switch open, lid open). **A common mistake is holding the switch actuator depressed and expecting an open event; that simulates a closed lid, not an open one.** The correct procedure:
-
-1. **Establish a closed baseline.** Press the switch actuator (or install the switch so the lid depresses it) so the pin reads LOW. Let at least one 30-second poll fire while the actuator is held closed — this records the LOW state in `prev_pin_mask` for that compartment. A faster shortcut: power the device on with the actuator already pressed. The first-boot code snapshots the current pin state as the baseline, so the LOW is captured immediately without waiting for a second poll.
-2. **Simulate a lid open.** Release the actuator (or open the lid) and leave it released until the next poll fires. The pin rises to HIGH, creating the LOW→HIGH transition the firmware detects as a lid-open event. The serial monitor should print `[open] compartment=N (DAY) day_mask=0bXXXXXXX`.
-3. **Verify in Notehub.** Confirm the corresponding `pill_open.qo` event appears in the **Events** tab within roughly two minutes (up to 30-second poll latency + cellular session establishment).
-
-If the actuator is pressed and released before the next poll fires, no event is generated; both transitions resolve within the same interval and the firmware observes no net change. This is expected behavior, not a fault.
-
-**Quick continuity check (no Notehub required).** Use a multimeter on the pin header or watch serial output while manually pressing and releasing the switch actuator: pressing it should pull the pin to ~0 V; releasing it should let the pin float to ~3.3 V (internal pull-up active). If the result is reversed, check for these common causes: wrong header pin, NO and NC terminals swapped, missing ground connection, actuator not engaging the lid properly, or internal pull-up not active — verify against the wiring table in §4.
-
-The daily summary is queued on the first wake after UTC midnight (or after the configured `summary_hour_utc` has passed on the new day) and arrives in Notehub on the next Notecard sync session — either the next scheduled outbound sync (default every 12 hours) or sooner if a `pill_open.qo` event triggers an immediate sync that day.
-
-**Power validation with Mojo.** Splice the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the LiPo and the Notecarrier CX +VBAT pin and run the device for 24 hours on the bench.
-
-**Notecard-datasheet figures** (from the [NOTE-MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) and the [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/)):
-
-| Phase | Published figure |
-|---|---|
-| Notecard idle (radio off, between sessions) | ~8–18 µA @ 5V |
-| Cellular session (modem active) | ~250 mA average, brief peaks up to ~2 A |
-
-**Modeled example budget** (estimated; validate with Mojo on your specific bench assembly before committing to a deployment charge schedule):
-
-| Phase | Estimated draw |
-|---|---|
-| Host MCU active (~150 milliseconds per 30 seconds wake) | ~10 mA for ~150 milliseconds every 30 seconds (~50 µA average contribution) |
-| Cellular session — open-event sync (`sync:true`, ~2–3 per day) | ~250 mA average, ~20–30 seconds per session |
-| Cellular session — scheduled outbound sync (every 12 h) | ~250 mA average, ~15–20 seconds per session |
-
-At 30-second polling with the default 12-hour outbound sync and roughly 2–3 open events per day triggering immediate syncs, the modeled daily energy total is on the order of **5–15 mAh/day**; treat this as an estimated starting point until confirmed with a bench trace.
-
-**Quick runway estimate:** With a 2500 mAh LiPo and 5–15 mAh/day, expect 5–15 months of continuous operation between charges (165–500 days). In a real patient deployment with variable cellular signal and temperature swings, always validate with a 24-hour Mojo trace before setting a charge schedule. Poor cellular signal or frequent refill sessions will increase daily consumption.
-
-A good Mojo trace on this device looks like: an essentially flat near-zero baseline punctuated by brief millisecond blips every 30 seconds (host wake), with occasional 15–30-second bursts at hundreds of milliamps (cellular session). If the baseline is continuously 10–80 mA, the host is not sleeping — check that the ATTN → EN path is intact on the Notecarrier CX and that `NotePayloadSaveAndSleep` is returning normally. If cellular bursts occur much more frequently than expected, verify `sync:true` is only set on open events and that the outbound cadence is not set to a very low value.
-
-With a 2500 mAh LiPo and an estimated 5–15 mAh/day, modeled runtime is roughly 165–500 days between charges. Actual runtime will vary with cellular signal strength, session frequency, and temperature; confirm with bench data before committing to a deployment charge schedule.
-
-Mojo is a **bench and commissioning tool** for this project; it is not deployed to the patient's home. Once a firmware revision passes the trace check, the deployed units run on LiPo without it.
-
-### Troubleshooting
-
-| Symptom | Likely cause | What to check |
-|---|---|---|
-| Device never appears in Notehub **Devices** tab. | `PRODUCT_UID` is empty or wrong, or the cellular antenna is disconnected. | Verify `PRODUCT_UID` matches the Notehub project exactly. Check that antennas are attached and not coiled tightly against the LiPo. Move the device near a window if testing indoors. |
-| Device appears but no `pill_open.qo` events show up after opening a lid. | Micro-switch mounting or wiring issue; pin wired to wrong header position. | Open the serial monitor at 115200 baud and watch for `[open]` lines. If none appear, confirm pin continuity with a multimeter: pin should read ~3.3 V with the lid open and ~0 V with the lid closed. |
-| `pill_open.qo` fires on startup without any lid being opened. | Likely a wiring fault floating the pin high, corrupted restored state where `prev_pin_mask` incorrectly records a compartment as closed, or a lid that physically moved between the first-boot pin snapshot and the following 30-second wake. | Check for loose wiring or wrong terminal (NC connected instead of NO) on the affected compartment pin. If the problem recurs after verifying wiring, force a first-boot reset by disconnecting the LiPo for ~30 seconds. On the next power-on the Notecard reports a cold boot, `NotePayloadRetrieveAfterSleep` returns false, and the firmware enters its `first_boot` path — re-snapshotting the current pin state rather than restoring any previous wake's state. |
-| Daily summary (`pill_summary.qo`) never appears. | UTC time not yet available (`utcDayAndHour()` returning 0), or `summary_hour_utc` hasn't been reached yet on the new day. | Confirm the device has had at least one successful cellular session (`_session.qo` present in Notehub). The summary is queued on the first wake after UTC midnight at or after `summary_hour_utc`, then delivered on the next Notecard sync session — the scheduled outbound sync (default every 12 hours) or sooner if a `pill_open.qo` event triggers an earlier sync. |
-| `opens_count` in the daily summary is missing (field absent from the Notehub event). | `full:true` was not applied. | Verify the firmware calls `note.add` with `JAddBoolToObject(req, "full", true)` before sending the summary. Without `full:true`, the template's omitempty behavior suppresses the zero value. |
-| Environment variable changes don't take effect. | Inbound sync hasn't occurred yet (default every 2 hours). | Use Notehub's **Sync Now** (inbound) button on the device page to trigger an immediate inbound sync. Alternatively, lower `inbound_min` in the fleet environment to `15`; the device re-applies `hub.set` automatically once it picks up the change, tightening the fetch cadence for subsequent updates. |
-| Mojo trace shows a continuous ~20 mA baseline instead of near-zero idle. | Host is not being put to sleep — ATTN is not gating the host power rail. | Confirm you're using a Notecarrier CX (which routes ATTN to EN). On a bare-board or different carrier, `NotePayloadSaveAndSleep` falls back to the software delay at the end of `sleepHost()`, which does not cut power. |
-| Mojo trace shows host stuck at active draw with no sleep cycles AND no `_session.qo` events arrive in Notehub. | Production-mode ATTN→EN power-gating fault — `NotePayloadSaveAndSleep` returned and the host did not lose power. The firmware halts after logging over USB serial. | Connect a USB cable and watch for the `[FATAL] NotePayloadSaveAndSleep returned and host did not lose power` line on the bench DIP switch's `HST` position. Inspect ATTN and EN wiring on the Notecarrier CX, then manually power-cycle (disconnect and reconnect the LiPo) to restart. The fault is intentionally not reported via `pill_diag.qo` because that `note.add` would race the Notecard's sleep transition and is unreliable. |
-| `pill_diag.qo` appears with `error: "pending_overflow"` or `"pending_overflow_cleared"`. | The 28-entry retry ring buffer filled while the Notecard was unable to accept `note.add` requests; the oldest buffered adherence events were evicted and lost. | Check for gaps in `_session.qo` events indicating a connectivity outage. The `dropped` field on the `pending_overflow` Note shows how many `pill_open.qo` events are missing. Once the Notecard recovers, the queue drains automatically and a `pending_overflow_cleared` Note confirms the episode is closed. Mark the affected interval in your downstream adherence record as incomplete. |
-
-If a problem isn't on this list, the [Blues community forum](https://discuss.blues.com) is the fastest place to get a second pair of eyes on a Notecard and sensor setup.
-
-
-## 10. Limitations and Next Steps
-
-The design deliberately stops at "did a lid open?", the most reliable signal that survives the realities of an elderly patient's home network, a caregiver's weekly refill routine, and a Bluetooth-fatigued smartphone. Confirmed ingestion, AM/PM trays, and local reminder UX are real product features, but they belong in a follow-on design rather than diluting the cellular-first sensor this POC is proving out.
-
-### Simplified for the POC
-
-The simplifications below are deliberate scope choices: each is a place where a production deployment will add a sensor, a downstream workflow, or a tighter data-handling control once a real RPM program starts running it. **This is a proof-of-concept reference design, not a medical device.**
-
-**Cannot confirm a dose was actually taken.** The device reports each detected compartment lid open, but it cannot distinguish "opened and took the pill" from "opened and closed without taking it." Confirming a dose requires a weight sensor on the tray or patient self-report; both are straightforward extensions but add hardware or UX complexity outside the scope of this POC.
-
-**Caregiver and pharmacist refill sessions are indistinguishable from patient dose-taking opens.** When the weekly tray is loaded — whether by the patient, a caregiver, or a pharmacist — opening each compartment lid to place pills generates `pill_open.qo` events and sets bitmask bits that are identical to genuine dose-taking opens. A single loading session can set all seven bits in `daily_opens` and produce up to seven events within one or two polling intervals, **creating a false picture of perfect adherence for that day.** This is a more active distortion than the dose-confirmation limitation above: it inflates the adherence record rather than leaving it ambiguous. Downstream systems and clinical workflows must account for this in at least one of the following ways:
- - **Fill outside the monitored window.** If the tray is loaded at a predictable time (e.g., Sunday evening before the monitoring week begins), configure downstream routes to flag or suppress clustered multi-lid opens during that window.
- - **Use a clinician-side refill flag.** The care coordinator logs refill sessions in the downstream covered system; the patient-record view excludes or tags those event clusters from adherence calculations.
- - **Filter simultaneous multi-lid opens downstream.** Multiple bits set in a single 30-second poll (`newly_opened` containing ≥ 3 compartments at once) is a strong operational signal of a refill rather than individual dose-taking. A downstream route rule or dashboard filter can flag clusters above a configurable threshold for manual review rather than counting them as adherence events.
-
-**Brief opens between polls are missed.** The firmware detects lid-open events by comparing pin state at each poll boundary (default every 30 seconds). A lid that is opened and fully re-closed within a single poll interval generates no event and is not counted in the daily summary. For the intended use case — a patient opening a compartment to take a pill — the lid will naturally remain open long enough to be detected. Caregiver testing, brief accidental knocks, or other sub-30-second interactions will not be recorded. Reducing `poll_interval_sec` to the firmware-enforced minimum of 15 seconds halves the exposure window; interrupt-driven or latch-based hardware would eliminate it entirely.
-
-**Pending retry queue has a finite depth.** If the Notecard is unable to accept `note.add` requests across more than four consecutive worst-case polling wakes (28 buffered events), the oldest queued dose-open records are evicted and **permanently lost.** The device emits a `pill_diag.qo` Note with `error: "pending_overflow"` and a `dropped` count when overflow first occurs, and a matching `error: "pending_overflow_cleared"` Note when the queue drains, providing cloud-visible evidence of data loss. A Notecard outage long enough to exhaust the buffer is uncommon on a battery-backed Notecard Cell+WiFi, but in the worst case downstream adherence calculations will undercount dose-opens for the affected interval without an explicit correction signal. Route `pill_diag.qo` to your alert channel alongside `pill_open.qo` so these episodes are not missed.
-
-**7-compartment design only.** The Notecarrier CX exposes exactly seven digital I/O pins (D5, D6, D9–D13), which maps cleanly to a standard 7-day tray. A 14-compartment tray (AM/PM per day) would require an I2C GPIO expander such as the MCP23017, adding one part and a library dependency.
-
-**No on-device time zone awareness.** The daily summary rolls over at UTC midnight. In practice, a care coordinator should set `summary_hour_utc` to match the patient's local midnight (e.g., `5` for Eastern Standard Time), which is tunable via environment variable without a reflash.
-
-**Micro-switch placement is manual.** Mounting the micro-switches inside a commercial pillbox requires drilling, adhesive, or 3D-printed brackets. A production design would integrate the switches into a purpose-built tray PCB.
-
-**No tamper or battery-low reporting.** The device does not currently alert if someone removes the Notecard, cuts power, or if the LiPo voltage drops below a safe threshold. Battery voltage monitoring via `card.voltage` and a `sync:true` low-battery event would be a straightforward addition.
-
-**No local visual or audible reminder.** The device is purely a reporting platform; it does not buzz or light up to remind the patient to take their medication. Adding a piezo buzzer on a PWM-capable pin would require a scheduled inbound event from Notehub to trigger it.
-
-**Pre-time-sync opens carry no calendar date.** On first boot, the device may accumulate opens before the Notecard acquires valid UTC time. Those opens remain in `daily_opens` and are associated with the first valid UTC day once time becomes available, then move into `prev_day_opens` at the next actual day rollover to feed the subsequent summary, with the correct bitmask. However, the summary's timestamp reflects when it was sent, not the (unknown) calendar day when the lids were opened. If accurate calendar-day attribution of pre-sync opens is required, wait for a `_session.qo` event in Notehub (confirming time-sync) before placing the device with the patient.
-
-**Firmware state holds only one pending day's summary data.** The `PillboxState` struct contains a single `prev_day_opens` slot. If two consecutive UTC-day boundaries pass while a pending summary has not yet been emitted (for example, because the device is powered off spanning an entire day, or because total power loss causes `NotePayloadRetrieveAfterSleep` to fail on the next boot), the slot is overwritten and the earlier day's adherence data is unrecoverable. Note that a lack of cellular connectivity alone does *not* cause this: the Notecard stores queued Notes locally in its on-device flash and delivers them automatically once connectivity is restored. The risk is power loss before `emitDailySummary()` executes. Size the LiPo for the intended deployment duration to minimize exposure.
-
-**Patient identity must stay out of Note payloads and Notehub metadata.** Note bodies are stored and routed as plaintext through Notehub and any downstream systems. Notehub device metadata (tags, serial-number field) is not designed as a covered system for protected health information. For an RPM deployment, **never embed a patient name, MRN, date of birth, or any other PHI in a Note body or in Notehub device metadata.** Assign only an opaque, non-PHI deployment identifier (for example, a random UUID or clinic-assigned device code) to the Notehub device tag or serial-number field. Maintain the mapping from device UID to patient record exclusively in your downstream covered system. If your deployment architecture requires routing PHI through any cloud component, engage qualified counsel to review the complete data path before go-live.
-
-### Production Next Steps
-
-Once a real RPM program is running the basic monitor, the following extensions are the natural progression toward a fieldable product.
-
-**14-slot tray support** via an MCP23017 I2C GPIO expander (adds AM/PM per day); the firmware's bitmask and event structure extend naturally to 16 bits.
-
-**Opaque deployment-ID assignment** (a random UUID or clinic-assigned device code) in the Notehub device tag or serial-number field so downstream routes can correlate events to the correct caregiver via a patient-record lookup in the downstream covered system, keyed on device UID.
-
-**Voltage-variable sync behavior** via `hub.set` `voutbound`, automatically extending the outbound cadence as the LiPo voltage drops, maximizing runtime without manual intervention.
-
-**[Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/)** for over-the-air host firmware updates so new features (buzzer support, new alert rules) can be pushed to deployed devices without a pharmacy recall.
-
-**A scheduled inbound Notefile** (`pill_reminder.qi`) that the Notecard can trigger a local buzzer or LED on, turning the device from a passive sensor into an active reminder system.
-
-
-## 11. Summary
-
-The care coordinator now has the one signal that matters: a `pill_open.qo` event when a lid opens, and a clean `opens_count: 0` at the end of any day the patient missed entirely. The box arrives on the patient's nightstand pre-provisioned, asks nothing of them, and reports over cellular whether the household's WiFi exists, works, or has been forgotten years ago. For the elderly chronic-disease patients who need this monitoring most, that's the difference between a connected device and a device that stays connected.
+A cellular remote patient monitoring device that catches missed medication doses by reading snap-action switches inside a weekly pillbox and uploading an event to Notehub each time a compartment lid opens.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/cellular-medication-adherence-pillbox/)**
diff --git a/85-cellular-medication-adherence-pillbox/banner.png b/85-cellular-medication-adherence-pillbox/banner.png
deleted file mode 100644
index 5c130197..00000000
Binary files a/85-cellular-medication-adherence-pillbox/banner.png and /dev/null differ
diff --git a/85-cellular-medication-adherence-pillbox/diagrams/01-system-architecture.svg b/85-cellular-medication-adherence-pillbox/diagrams/01-system-architecture.svg
deleted file mode 100644
index 6f69e851..00000000
--- a/85-cellular-medication-adherence-pillbox/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/85-cellular-medication-adherence-pillbox/diagrams/02-wiring-assembly.svg b/85-cellular-medication-adherence-pillbox/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 2f206a66..00000000
--- a/85-cellular-medication-adherence-pillbox/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,83 +0,0 @@
-
diff --git a/85-cellular-medication-adherence-pillbox/diagrams/03-data-flow.svg b/85-cellular-medication-adherence-pillbox/diagrams/03-data-flow.svg
deleted file mode 100644
index 7bd41be9..00000000
--- a/85-cellular-medication-adherence-pillbox/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,81 +0,0 @@
-
diff --git a/87-post-discharge-vitals-relay-hub/README.md b/87-post-discharge-vitals-relay-hub/README.md
index f5f71e05..8300b38b 100644
--- a/87-post-discharge-vitals-relay-hub/README.md
+++ b/87-post-discharge-vitals-relay-hub/README.md
@@ -1,516 +1,5 @@
# Post-Discharge Vitals Relay Hub
-
+A cellular remote patient monitoring hub for post-discharge recovery programs that scans for BLE-enabled medical devices and relays completed readings to Notehub, with immediate alerts on clinical threshold exceedances.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Post-Discharge%20Vitals%20Relay%20Hub).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/post_discharge_vitals_hub/](firmware/post_discharge_vitals_hub/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [remote patient monitoring](https://blues.com/remote-patient-monitoring/) hub for 30–60-day post-discharge recovery programs. A Blues Notecard Cell+WiFi paired with an nRF52840 host that has native Bluetooth Low Energy scans for a patient's BLE-enabled medical devices (weight scale, blood pressure cuff, pulse oximeter, and activity band), relays each completed reading to the [Blues Notehub](https://blues.com/notehub/) cloud service over cellular, and immediately syncs readings that exceed configurable clinical thresholds so the care team is alerted without waiting for the next scheduled upload. No WiFi required, no app to configure, no network credentials to enter. Plug it in and it works.
-
-## 1. Project Overview
-
-
-
-**⚠️ Proof-of-concept only, not a medical device.** This is a reference design intended for technical evaluation and developer education. It is **not** a cleared or certified medical device, is **not** validated for diagnostic use, and must **not** be used for emergency response or life-sustaining patient monitoring. Any deployment that collects, stores, or routes patient health data must address patient-data governance (including HIPAA/PHI compliance where applicable), secure data routing and storage, data retention and auditability requirements, and all applicable regulatory obligations. Consult qualified clinical, legal, and compliance teams before collecting real patient data.
-
-
-
-**The problem.** Hospital discharge coordinators who manage heart failure, COPD (chronic obstructive pulmonary disease), post-surgical, and high-risk diabetic patients increasingly send patients home with a pack of BLE-enabled monitoring devices: a connected scale to catch fluid retention, a BP (blood pressure) cuff to watch for hypertension spikes, a pulse oximeter to flag respiratory deterioration, and an activity band to monitor resting heart rate. The clinical evidence for remote patient monitoring in this window is strong: daily weight checks and BP readings reduce 30-day readmissions, and early intervention on a trending SpO2 (blood oxygen saturation) drop catches pneumonia or PE (pulmonary embolism) while it is still outpatient-treatable.
-
-The weak link is the hub. Existing RPM programs ship one of two configurations: a dedicated tablet the patient has to set up, or a companion phone app the patient has to install. Both break constantly. The tablet needs a WiFi network that may not exist or may require a password the patient can't find; the phone app needs a compatible phone and a patient who is comfortable installing software. In practice, a significant share of enrolled patients never receive a reading upload during their first week simply because the connectivity layer fails them.
-
-**Why Notecard.** A hub with a cellular radio in it removes every one of those failure modes. It doesn't need the patient's WiFi password. It doesn't need a companion app. It doesn't need IT to add a device to the guest network. It ships pre-provisioned with a global SIM, and the moment the patient plugs it into a wall outlet the hub is online. This is exactly the zero-touch deployment model that the cellular Notecard was built for.
-This is the definition of a device that needs to work for everyone, including an 82-year-old heart failure patient who has never configured a router. The hub is the one piece of the care program that the program cannot afford to have the patient debug. Cellular eliminates the failure mode; the Notecard delivers the cellular with a SIM, an antenna, and a two-line I2C interface: no modem AT commands, no socket management, no session state machine.
-
-
-
-**Deployment scenario.** A small enclosure, roughly phone-charger-sized, mailed to the patient at discharge, along with the BLE device pack. The patient plugs the USB-C cable into the included charger, sets the hub on the nightstand, and forgets about it. The BLE devices use standard Bluetooth SIG health profiles (Weight Scale Service, Blood Pressure Service, Pulse Oximeter Service, Heart Rate Service). For indication-based devices (weight scale, blood pressure cuff, pulse oximeter), the device self-disconnects after transmitting one measurement and the hub immediately resumes scanning; the full connection cycle typically finishes in 2–5 seconds. Heart Rate Service devices such as the Polar H10 behave differently: they use Notification rather than Indication and stream samples continuously while worn. The firmware handles this by disconnecting after the first HR notification and suppressing reconnection for 15 minutes (`HR_SAMPLE_INTERVAL_MS`), so a patient wearing the band for two hours yields a bounded set of HR Notes rather than a continuous stream. The care team sees readings in Notehub, routed to whatever RPM dashboard or EHR integration the program uses, without any patient involvement after the initial plug-in.
-
-**Device identity model.** The hub is fail-closed by default: only BLE-bonded devices (primary gate) or devices whose MAC address appears in the allow-list in `vitals_config.h` (secondary gate for stable-address devices) are connected. The primary gate uses the nRF52840 SoftDevice's IRK resolution: when a bonded device advertises with a Resolvable Private Address, the SoftDevice resolves it using the stored IRK and sets `addr_id_peer=1` in the scan report before the application layer sees it. Bond keys are written to flash automatically and loaded at every boot. During initial commissioning, build with both `ALLOW_UNENROLLED_DEVICES_FOR_DEV=1` and `ALLOW_COMMISSIONING_BUILD` (both flags required together) to allow connections to unrecognized devices so pairing can take place. After commissioning, add any public/static-address devices to `ENROLLED_DEVICES` in `vitals_config.h`, then rebuild with both flags removed. See the **Commission** step in §3 Technical Summary for the complete enrollment flow.
-
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** The hub the patient plugged in is doing two jobs at once. The Adafruit Feather nRF52840 Express runs Nordic's certified SoftDevice as a BLE Central — scanning for advertisements that match a target service UUID, connecting to the first one it sees, subscribing to the right GATT characteristic (indication for weight, BP, and SpO2; notification for heart rate), and decoding the raw bytes (weight as a fixed-point uint16; blood pressure and SpO2 as IEEE 11073 SFLOAT; heart rate as a plain integer). The decoded reading is then handed across I²C to the Notecard through the Notecarrier F's Feather header, using `note-arduino`'s `JAdd*` helpers to build the JSON request. The host never sees a modem, an AT command, or a socket.
-
-**Notecard responsibilities.** Once the reading lands in the Notecard's on-device queue, the radio takes over. The Notecard opens a cellular session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 15 minutes) and flushes any Note marked `sync:true` immediately. Coming back the other way, it distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) pushed from Notehub, so a care coordinator can tighten or loosen the alert thresholds for a specific patient without anyone touching the firmware.
-
-**Notehub responsibilities.** [Notehub](https://dev.blues.io/notehub/notehub-walkthrough/) ingests, stores, and applies project [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) to every event. The four reading Notefiles (`weight.qo`, `bp.qo`, `spo2.qo`, `activity.qo`) and the alert Notefile (`vitals_alert.qo`) stay separate so they can fan out at different urgencies: readings to a long-term analytics store, alerts to an on-call channel or EHR endpoint in near-real time. [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices by patient cohort or care program, and [smart fleet rules](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) auto-assign newly claimed devices to the right fleet based on serial number or provisioning tag.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP Pub/Sub, and several other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-
-## 3. Technical Summary
-
-**Before you start — prerequisites:**
-
-- Notehub account and a project (to get your ProductUID)
-- Arduino IDE with Adafruit nRF52 board support package installed
-- USB-C and Micro-USB cables for power and firmware flashing
-- At least one test BLE health device (weight scale, BP cuff, pulse oximeter, or activity band) for commissioning
-
-To go from parts on the bench to first event in Notehub as quickly as possible:
-
-1. **Notehub:** create a [Notehub project](https://notehub.io), copy its ProductUID.
-2. **Assemble:** seat the nRF52840 Feather on the Notecarrier F, slot the Notecard into the M.2 connector, and plug Mojo inline on the power rail for bench validation.
-3. **Edit one line:** set `PRODUCT_UID` in [`firmware/post_discharge_vitals_hub/vitals_config.h`](firmware/post_discharge_vitals_hub/vitals_config.h). For the initial commissioning flash, also add **both** `-DALLOW_UNENROLLED_DEVICES_FOR_DEV=1` **and** `-DALLOW_COMMISSIONING_BUILD` to your build flags — both are required together; neither may appear in the shipping build.
-4. **Flash:** select **Adafruit Feather nRF52840 Express** in the Arduino IDE and upload.
-5. **Commission:** bring each patient BLE device within range and initiate a measurement. The hub connects, subscribes, and initiates BLE pairing; bond keys are stored in flash. Once all devices are bonded, open Notehub → `commissioning.db` and inspect the `bond_established` Notes: each Note shows the device's `ble_addr` and `addr_type`. For any device whose `addr_type` is `0x00` (public) or `0x01` (random static), add its address to `ENROLLED_DEVICES` in `vitals_config.h`; those devices advertise with a fixed address that the hub cannot identify through IRK resolution alone. Devices with `addr_type` `0x02` (random private resolvable / RPA) are recognized automatically via the stored IRK and need no `ENROLLED_DEVICES` entry. Devices with `addr_type` `0x03` (random private **non**-resolvable) are **not** supported in the fail-closed production flow — non-resolvable addresses rotate without any IRK linkage so the hub cannot re-identify them across address changes; a patient device that reports `addr_type` `0x03` requires a different identity mechanism before it can be used in a production deployment. Once all entries are added, remove **both** `-DALLOW_UNENROLLED_DEVICES_FOR_DEV=1` **and** `-DALLOW_COMMISSIONING_BUILD` from your build flags and reflash. The hub is now fail-closed: only bonded and enrolled devices are accepted.
-6. **Watch:** open Notehub → Events. A `_session.qo` appears within a minute; a normal reading Note appears in the appropriate Notefile within the next 15-minute outbound window. If the reading trips a threshold, `sync:true` is set on **both** the measurement Note and `vitals_alert.qo` — both typically appear in Notehub within the same immediate cellular session (~15–60 s), or in back-to-back immediate sessions.
-
-
-Here is a sample Note this device emits:
-```json
-{
- "weight_kg": 84.35,
- "prev_kg": 82.10
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier F](https://shop.blues.com/products/notecarrier-f?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Feather-compatible carrier with M.2 slot for the Notecard, onboard LiPo connector, and Feather-header I²C routed to the Notecard — accepts any Feather-format MCU. |
-| [Notecard Cell+WiFi](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([MBGLW](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular removes the WiFi dependency for the patient; no credential provisioning needed. Ships with a global prepaid SIM — no activation, no monthly commitment per device. |
-| Cellular antenna, adhesive flex, u.FL connector — included in Notecarrier F package | 1 | Adhesive planar antenna adequate for home and office environments. Verify it is in the box before assembly; the Notecard will not register without a connected antenna. |
-| u.FL to SMA bulkhead pigtail + external LTE whip antenna *(optional, metal-enclosure installs only)* | 0–1 | Required if the hub is installed inside a metal enclosure or in a room with poor indoor signal. Route the SMA bulkhead through a drilled port in the enclosure wall and connect the LTE whip outside. |
-| [Adafruit Feather nRF52840 Express](https://www.adafruit.com/product/4062) | 1 | Nordic nRF52840 SoC with native BLE 5.0 — the BLE radio that scans and connects to the patient's device pack. Feather form factor seats directly in the Notecarrier F. The board uses a **Micro-USB** port for firmware flashing and serial monitoring. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter for bench-validating cellular session energy and BLE scanning current during bring-up. Not required in the deployed hub. |
-| USB-C wall adapter, 5V/2A | 1 | Hub is wall-powered via the Notecarrier F's USB-C port. |
-| USB-C cable, ~1.8 m / 6 ft | 1 | Gives the patient enough reach from a bedside outlet to a nightstand. Powers the hub via the Notecarrier F's USB-C port. |
-| Micro-USB cable (development only) | 1 | Required to flash firmware from the Arduino IDE and monitor serial output via the Feather's Micro-USB port. Remove from the patient kit before shipping. |
-| Project enclosure, ~100 × 65 × 30 mm | 1 | Clear polycarbonate lid lets the care team see the power LED remotely on a telehealth video call. |
-
-**BLE patient devices — bench-validation set.** The firmware implements Bluetooth SIG standard service profiles and should work with any compliant device. The four models below are the specific devices used to validate each parser; interoperability with untested devices is not guaranteed. See §7.4 and §10 Limitations for parser scope details, especially for the Blood Pressure device.
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Withings Body (WBS06)](https://www.withings.com/us/en/body) | 1 | Bench-validation weight scale. Implements Weight Scale Service (0x181D) / Weight Measurement (0x2A9D) in SI mode. Used to validate `parseWeightKg`. |
-| [Omron M4 Intelli IT (HEM-7155T)](https://www.omron-healthcare.com/products/m4-intelli-it) | 1 | Bench-validation blood pressure cuff. Implements Blood Pressure Service (0x1810) with the timestamp optional field set. Used to validate `parseBpMmhg`. The parser walks the optional-field flags to compute the pulse-rate offset correctly for both timestamp-present and timestamp-absent layouts — see §7.4. |
-| [Nonin 3230](https://www.nonin.com/products/3230/) | 1 | Bench-validation pulse oximeter. Implements Pulse Oximeter Service (0x1822) / PLX Spot-Check Measurement (0x2A5E). Used to validate `parseSpO2`. |
-| [Polar H10](https://www.polar.com/us-en/sensors/h10-heart-rate-sensor/) | 1 | Bench-validation heart-rate chest strap. Implements Heart Rate Service (0x180D) / Heart Rate Measurement (0x2A37) in 8-bit HR value format. Used to validate `parseHeartRate`. The Polar H10 streams notifications continuously while worn; the firmware disconnects after the first sample and suppresses reconnection for `HR_SAMPLE_INTERVAL_MS` (15 minutes) — see §7.3. |
-
-The Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no per-device activation fees and no monthly cellular subscription requirement.
-
-
-## 5. Wiring and Assembly
-
-
-
-No sensor wiring is required — all patient data arrives over BLE. The electrical connections are the stacked-board I²C bus, the shared power rail, and the antenna. The table below lists each signal explicitly; steps 1–5 describe the assembly sequence.
-
-**Pin-by-pin connections:**
-
-| Signal | Feather nRF52840 pin | Notecarrier F header pin | Notecard / destination | Notes |
-|---|---|---|---|---|
-| I²C data (SDA) | `SDA` (labeled pad, nRF52840 P0.26) | Feather header pin 6 (`SDA`) | Notecard `SDA` | Notecarrier F has 10 kΩ pull-up to 3.3V on-board; no external pull-up needed. |
-| I²C clock (SCL) | `SCL` (labeled pad, nRF52840 P0.27) | Feather header pin 7 (`SCL`) | Notecard `SCL` | Same pull-up network as SDA. |
-| 3.3V logic rail (VIO) | `3V3` (Feather header pin 2) | Feather header 3.3V rail | Notecard VIO + Feather MCU logic supply | Generated by the Notecarrier F's onboard LDO. Supplies logic/VIO only; the Notecard's cellular modem and radio draw main power from the Notecarrier F's USB-C input via a separate path on the M.2 connector. |
-| Ground | `GND` (Feather header pin 1) | Feather header GND | Notecard GND | Common ground for the entire stack. |
-| Cellular antenna | — | — | Notecard MBGLW cellular u.FL port (`MAIN`) | Connect the adhesive pigtail u.FL plug here. Adhere the flat antenna patch to the inside of the enclosure lid or a non-metallic enclosure wall away from the Notecard edge. |
-
-All connections from Feather to Notecard travel through the stacked Feather header — no jumper wires are required for the I²C bus or power rail.
-
-1. **Seat the Notecard.** Insert the Notecard Cell+WiFi into the M.2 Key-E slot on the Notecarrier F and secure the single M.2 retention screw. Connect the adhesive cellular antenna's u.FL plug to the cellular `MAIN` u.FL port on the Notecard edge (the port closest to the M.2 connector). Adhere the antenna patch to a flat, non-metallic surface — the inside of the enclosure lid works well. For metal enclosures or concrete-wall installations, use the optional u.FL-to-SMA bulkhead pigtail listed in §3, thread it through a cable-gland port in the enclosure wall, and connect an external LTE whip outside.
-
-2. **Seat the Feather.** Press the Adafruit Feather nRF52840 Express onto the Notecarrier F's two 16-pin Feather headers. The Feather's `SDA` and `SCL` pads (header pin 6 and 7 respectively) mate with the Notecarrier F's I²C pads, which route directly to the Notecard's `SDA`/`SCL` pins. The 3.3V supply and GND are similarly routed through the header — no jumper wires are needed.
-
-3. **Mojo (bench only).** Splice the Mojo inline between the USB-C power supply and the Notecarrier F's USB-C input to measure total stack current, or use the Notecarrier F's JST-PH battery connector with a bench LiPo to isolate the stack from USB ground noise. Connect the Mojo's Qwiic cable to the Notecarrier F's Qwiic port (GND / 3.3V / SDA / SCL at 3.3V logic). The Qwiic bus is shared with the Feather header I²C lines — the Notecard communicates with the Mojo's LTC2944 coulomb counter over the same bus.
-
-4. **Power and programming ports.** The Notecarrier F supplies the stack through two distinct paths: (a) **main power** — USB-C 5V feeds the Notecard's M.2 power pins directly, driving the cellular modem and radio; (b) **3.3V logic rail** — the Notecarrier F's onboard LDO converts that same USB supply to 3.3V and distributes it to the Feather header `3V3` pin (powering the Feather MCU) and the Notecard VIO pin (I/O level reference). These paths are separate: the Notecard's radio circuitry draws from main power, not from the 3.3V logic rail.
-
- **Deployment:** plug the USB-C cable from the wall adapter into the **Notecarrier F's USB-C port**. This single connection powers both paths and drives the entire stack.
-
- **Development and firmware flashing:** both ports must be connected simultaneously. Connect a Micro-USB cable to the **Feather's Micro-USB port** for firmware upload and serial monitoring — the Feather's UF2 bootloader enumerates over its own Micro-USB connector; the Notecarrier F's USB-C port cannot flash the Feather. At the same time, keep the **Notecarrier F's USB-C port** connected to a USB supply so the Notecard has main power. Connecting only the Feather Micro-USB leaves the Notecard without main power and all Notecard API calls will time out.
-
-5. **Enclosure.** The Notecarrier F (approximately 96 × 62 mm) fits in a 100 × 65 × 30 mm or larger enclosure. Drill a single pass-through for the USB-C power cable on one side and a small vent on the opposite side (the nRF52840 and Notecard generate modest heat during a cellular session). For home deployments with the adhesive antenna, no external antenna port is needed.
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) — it looks like `com.your-company.your-name:vitals-hub`.
-
-2. **Set the ProductUID in firmware.** Open [`vitals_config.h`](firmware/post_discharge_vitals_hub/vitals_config.h) and add `#define PRODUCT_UID "com.your-company.your-name:vitals-hub"` at the very top of the file, above the `#ifndef PRODUCT_UID` guard — replacing the quoted string with your actual ProductUID. Alternatively, pass it as a compiler flag: `-DPRODUCT_UID='"com.your-company.your-name:vitals-hub"'`. `PRODUCT_UID` is defined through `vitals_config.h`; there is no `#define PRODUCT_UID` in the `.ino` file.
-
-3. **Claim the Notecard.** Power the assembled hub via USB-C. On first cellular connection the Notecard associates itself with your Notehub project automatically — no manual claim step. The device appears in the **Devices** tab within 1–2 minutes.
-
-4. **Create a Fleet per patient cohort.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. A natural structure for an RPM program is one fleet per condition (e.g., `chf-30-day`, `copd-60-day`) so that threshold defaults can be tuned at the fleet level and overridden per device when a specific patient's care plan calls for tighter limits. [Smart Fleet rules](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can auto-assign devices at provisioning time based on a tag set during kitting.
-
-5. **Set environment variables.** In Notehub: **Devices** → your device → **Environment** (or at the Fleet level for the default that applies to all devices in the cohort). All variables are optional — firmware defaults are shown below.
-
-
-
- **Clinical disclaimer.** The default threshold values and any clinical commentary in this table are **illustrative starting points for a proof-of-concept demonstration only**. They are not medical advice and do not represent validated clinical standards. All alert thresholds must be selected, reviewed, and validated by the clinical program team for the intended patient population and care workflow before this device is used in any patient-facing setting.
-
-
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `bp_systolic_high` | `160` | Systolic BP threshold (mmHg) above which `bp_high` alert fires. Configurable program threshold — AHA Stage 2 hypertension begins at ≥140 mmHg systolic (≥90 diastolic), but many RPM programs set a higher initial alert point (e.g. 160/100) to reduce alarm fatigue. |
- | `bp_diastolic_high` | `100` | Diastolic BP threshold (mmHg) above which `bp_high` alert fires (in combination with or independently of the systolic threshold). |
- | `spo2_low` | `92` | SpO2 threshold (%) below which `spo2_low` alert fires. Values under 92% in a recovering patient warrant clinical review. |
- | `hr_high` | `130` | Heart rate threshold (bpm) above which `hr_high` alert fires. **Applied only to Heart Rate Service (0x180D) readings from the activity band.** The pulse values carried in blood pressure (`bp.qo`) and SpO2 (`spo2.qo`) Notes are recorded but not evaluated against this threshold. |
- | `hr_low` | `40` | Heart rate threshold (bpm) below which `hr_low` alert fires. Same scope: activity band Heart Rate Service readings only. |
- | `weight_delta_kg` | `2.3` | Weight gain threshold (kg, ≈ 5 lbs): fires `weight_gain` when the current reading exceeds the previous reading in the same boot session by this amount. Delta resets to zero on power cycle (no cross-reboot persistence in the POC). This is the standard CHF fluid-retention threshold in most heart failure disease management programs. |
-
- Changes take effect on the hub's next inbound sync (default 60 minutes). To pull updated values sooner, temporarily reduce `inbound` in the Notecard's `hub.set` configuration, or trigger a sync from the [blues.dev In-Browser Terminal](https://dev.blues.io/terminal). The Notecard fetches fresh env-var values from Notehub on each inbound session (default 60 minutes); the firmware then re-reads those locally cached values every 2 minutes (`ENV_POLL_MS`) via `env.get` — a call resolved on the Notecard without a cellular round-trip — so threshold changes apply within 2 minutes of the next inbound sync rather than sitting unused for up to a full additional hour.
-
-6. **Configure routes.** Add at minimum one route for `vitals_alert.qo` (real-time delivery to an on-call endpoint, SMS gateway, or EHR webhook) and one for each reading Notefile (to a long-term analytics store). Splitting the Notefiles lets you route alert Notes to a paging system and routine readings to a patient record without any filtering logic in the route itself. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub).
-
-### What to look for in Notehub
-
-In the Notehub web console, navigate to **Devices** (your newly claimed hub), then click **Events** to see real-time and historical Notes. You will see these Notefiles:
-
-- **`_session.qo`** — appears within a minute of first power-on. Confirms the Notecard has cellular coverage and your ProductUID is correct. Each inbound sync (once per hour by default) and each threshold-triggered outbound sync generates a session Note.
-- **`weight.qo`**, **`bp.qo`**, **`spo2.qo`**, **`activity.qo`** — normal readings (those that do not trip a threshold) queue on the Notecard and appear in Notehub within the next 15-minute outbound window. Readings that do trip a threshold carry `sync:true` on the measurement Note itself, so that reading and its companion `vitals_alert.qo` Note typically arrive in the same immediate cellular session, or in back-to-back immediate sessions — not on the next scheduled sync.
-- **`vitals_alert.qo`** — emitted alongside the threshold-tripping measurement Note, also with `sync:true`. Both Notes typically arrive in the same immediate cellular session (~15–60 s), or in back-to-back immediate sessions — bypassing the normal 15-minute upload timer. Check the `body` of each alert Note to see which threshold was crossed (e.g., `{"alert":"bp_high","systolic_mmhg":168,"diastolic_mmhg":98}`).
-- **`commissioning.db`** — written during a commissioning build (both `ALLOW_UNENROLLED_DEVICES_FOR_DEV=1` and `ALLOW_COMMISSIONING_BUILD` active) when the hub successfully bonds a new device. Each `bond_established` Note contains the device's `ble_addr` (colon-separated, MSB-first) and `addr_type`. Inspect these Notes after commissioning: devices with `addr_type` `0x00` (public) or `0x01` (random static) must be added to `ENROLLED_DEVICES` in `vitals_config.h` before the shipping build — see the **Commission** step in §3 Technical Summary and §10 Limitations.
-
-
-## 7. Firmware Design
-
-| File | Role |
-|------|------|
-| [`post_discharge_vitals_hub.ino`](firmware/post_discharge_vitals_hub/post_discharge_vitals_hub.ino) | Arduino entry point: global state, `setup()`, `loop()` |
-| [`vitals_config.h`](firmware/post_discharge_vitals_hub/vitals_config.h) | Build-time configuration: `PRODUCT_UID`, `DEBUG_VITALS`, `ALLOW_UNENROLLED_DEVICES_FOR_DEV`, interval constants, MAC allow-list |
-| [`ble_central.h`](firmware/post_discharge_vitals_hub/ble_central.h) / [`ble_central.cpp`](firmware/post_discharge_vitals_hub/ble_central.cpp) | BLE Central: identity gate, scan/connect/disconnect callbacks, GATT data callbacks, pairing initiation, `initBLE()` |
-| [`notecard_helpers.h`](firmware/post_discharge_vitals_hub/notecard_helpers.h) / [`notecard_helpers.cpp`](firmware/post_discharge_vitals_hub/notecard_helpers.cpp) | Notecard layer: `hub.set` configuration, template registration, env-var fetch, Note submission with checked alert delivery |
-| [`ble_parsers.h`](firmware/post_discharge_vitals_hub/ble_parsers.h) | Static inline GATT decoders + physiological plausibility guards |
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Adafruit nRF52 Arduino** board support package — install via the Arduino IDE Boards Manager by adding the index URL `https://adafruit.github.io/arduino-board-index/package_adafruit_index.json` under **File → Preferences → Additional Boards Manager URLs**, then searching for "Adafruit nRF52". Select **Adafruit Feather nRF52840 Express** as the board.
-- **Blues Wireless Notecard** (`note-arduino`) — install via the Arduino Library Manager (search "Blues Wireless Notecard"). Check [github.com/blues/note-arduino/releases](https://github.com/blues/note-arduino/releases) for the latest release before flashing.
-- **bluefruit.h** — included with the Adafruit nRF52 board support package; no separate install required.
-
-**Flashing:** Connect a **Micro-USB cable** to the **Feather's Micro-USB port** (not the Notecarrier F's USB-C port). Open `post_discharge_vitals_hub.ino` in the Arduino IDE, select **Adafruit Feather nRF52840 Express**, select the correct COM/tty port (the Feather enumerates as a serial device over its Micro-USB connector), and click **Upload**. No external programmer is needed — the Feather's onboard UF2 bootloader handles programming directly over USB.
-
-Open the serial monitor at **115200 baud** to watch `[BLE]` and `[VITALS]` log lines in real time during bench bring-up — **debug build only** (`DEBUG_VITALS=1` with `ALLOW_DEBUG_BUILD`, per the build flags above). The default shipping build emits no serial output; patient data is never printed over USB in a production flash.
-
-**Build flags.** Several compile-time flags used in this project are guarded by a companion acknowledgment flag; omitting the companion produces a hard compile-time error so a development or commissioning build cannot be accidentally shipped to a patient hub. The cleanest way to pass extra flags is with **`arduino-cli`**:
-
-```sh
-# Debug build (verbose serial logging)
-arduino-cli compile \
- --fqbn adafruit:nrf52:feather52840 \
- --build-property "compiler.cpp.extra_flags=-DALLOW_DEBUG_BUILD -DDEBUG_VITALS=1" \
- --build-property "compiler.c.extra_flags=-DALLOW_DEBUG_BUILD -DDEBUG_VITALS=1" \
- firmware/post_discharge_vitals_hub
-
-# Commissioning build (identity bypass for initial BLE pairing)
-arduino-cli compile \
- --fqbn adafruit:nrf52:feather52840 \
- --build-property "compiler.cpp.extra_flags=-DALLOW_UNENROLLED_DEVICES_FOR_DEV=1 -DALLOW_COMMISSIONING_BUILD" \
- --build-property "compiler.c.extra_flags=-DALLOW_UNENROLLED_DEVICES_FOR_DEV=1 -DALLOW_COMMISSIONING_BUILD" \
- firmware/post_discharge_vitals_hub
-```
-
-Arduino IDE users can add extra flags by creating a `platform.local.txt` file in the Adafruit nRF52 board-support-package folder and adding a line such as `compiler.cpp.extra_flags=-DALLOW_DEBUG_BUILD -DDEBUG_VITALS=1` (see the [Arduino platform customization docs](https://arduino.github.io/arduino-cli/latest/platform-specification/#platformlocaltxt)). The **Sketch → Export Compiled Binary** menu does not expose a build-flags field in the standard IDE GUI for the nRF52 board package.
-
-### 7.2 Module responsibilities
-
-| Responsibility | Where |
-|---|---|
-| Build-time config, DBG macros, MAC allow-list | `vitals_config.h` |
-| Notecard hub configuration and template registration | `notecard_helpers.cpp` — `notecardConfigure`, `defineTemplates` |
-| Threshold fetch from Notehub env vars | `notecard_helpers.cpp` — `fetchEnvVars` (boot + every 2 minutes via `ENV_POLL_MS`) |
-| Reading Note emission + checked alert delivery | `notecard_helpers.cpp` — `submitWeight`, `submitBp`, `submitSpO2`, `submitActivity` |
-| Device identity gate (bonding + MAC allow-list) | `ble_central.cpp` — `isIdentifiedDevice` |
-| BLE scan + connect + disconnect + pairing event handling | `ble_central.cpp` — `bleScanCallback`, `bleConnectCallback`, `bleDisconnectCallback`, `blePairCompleteCallback` |
-| GATT indication/notification callbacks + plausibility checks | `ble_central.cpp` — `weightDataCallback`, `bpDataCallback`, `spo2DataCallback`, `hrDataCallback` |
-| BLE characteristic byte parsing + plausibility helpers | `ble_parsers.h` — `parseSfloat`, `parseWeightKg`, `parseBpMmhg`, `parseSpO2`, `parseHeartRate`, `weightKgPlausible`, `bpPlausible`, `spo2Plausible`, `hrPlausible` |
-| Buffered reading dispatch and watchdog | `post_discharge_vitals_hub.ino` — `loop()` |
-
-### 7.3 BLE scanning and GATT reading strategy
-
-The nRF52840 SoftDevice operates as a BLE Central. `bleScanCallback` fires for every advertisement packet received; the firmware calls `Bluefruit.Scanner.checkReportForService()` against each of the four target service UUIDs and connects only on a match. On connection, `bleConnectCallback` calls `service.discover(connHandle)` to identify which of the four device types just connected, then calls `char.discover()` followed by `enableIndicate()` (for weight, BP, and SpO2, which use the GATT Indication sub-procedure) or `enableNotify()` (for heart rate, which uses Notification). The SoftDevice handles GATT ATT acknowledgment automatically. When the device transmits a reading, the corresponding data callback populates a buffered struct and sets its `valid` flag; the main loop drains that buffer on the next iteration.
-
-One BLE Central connection slot is used at a time. If the patient picks up the BP cuff while the scale is connected, the connection to the scale finishes first (scale sends one indication and disconnects), the scanner restarts (via `restartOnDisconnect(true)`), and the BP cuff is picked up on the next scan cycle.
-
-**Indication-based devices (weight, BP, SpO2)** self-disconnect after sending a single measurement; the full connection-measure-disconnect cycle typically completes within 2–5 seconds.
-
-**Heart Rate Service devices** use Notification rather than Indication and stream readings continuously for as long as they remain connected — a device like the Polar H10 will keep sending samples every second indefinitely. The firmware handles this explicitly: `loop()` calls `Bluefruit.disconnect()` after consuming the first HR notification, and `bleScanCallback` suppresses further HR Service connection attempts for `HR_SAMPLE_INTERVAL_MS` (15 minutes) by checking `millis() - g_last_hr_sample_ms`. This bounds `activity.qo` Note rate to at most one sample per 15-minute window while the band is within range, regardless of how long the patient wears it.
-
-The four target service UUIDs are assigned by the Bluetooth SIG:
-
-| Device | Service UUID | Characteristic UUID | Sub-procedure |
-|--------|-------------|---------------------|---------------|
-| Weight scale | 0x181D | 0x2A9D | Indication |
-| Blood pressure cuff | 0x1810 | 0x2A35 | Indication |
-| Pulse oximeter | 0x1822 | 0x2A5E | Indication |
-| Activity band | 0x180D | 0x2A37 | Notification |
-
-### 7.4 Characteristic value encoding
-
-The four measurement characteristics use three different wire encodings; it is not uniform SFLOAT throughout the stack.
-
-**Blood pressure and SpO2 — IEEE 11073 SFLOAT.** The Blood Pressure Measurement (0x2A35) and PLX Spot-Check Measurement (0x2A5E) characteristics encode their numeric fields as IEEE 11073 SFLOAT: a 16-bit type where the upper four bits are a signed exponent and the lower twelve bits are a signed mantissa (value = mantissa × 10^exponent). The firmware decodes these in `parseSfloat()` (in `ble_parsers.h`). Reserved sentinel values (0x07FF = NaN, 0x0800 = NaN, 0x07FE = +Inf, 0x0802 = -Inf, 0x0801 = Reserved) are compared against the **full 16-bit raw SFLOAT encoding** before decoding mantissa and exponent. Checking only the masked 12-bit mantissa field would incorrectly reject valid numbers. For example, `0x17FF` has mantissa bits `0x7FF` that match the NaN sentinel mask, but its exponent field (`0x1`) makes it a valid number (mantissa=2047, exponent=1, value=20470). Results are rounded to integers (1 mmHg / 1% granularity is clinically sufficient).
-
-**Blood Pressure parser scope.** Systolic and diastolic values (bytes [1–6], always mandatory) parse correctly for any standards-compliant BP device. The optional `pulse_bpm` field is decoded by walking the optional-field flags: the parser computes the pulse-rate SFLOAT offset as 7 bytes (mandatory header) + 7 bytes if the timestamp field is present (flags bit 1). This correctly handles both the timestamp-included layout (the Omron M4 bench-validation device) and the timestamp-absent layout, covering all compliant Blood Pressure Measurement (0x2A35) implementations. User ID (flags bit 3) and Measurement Status (flags bit 4) fields appear after the pulse rate in the spec ordering and are not captured.
-
-**PLX Spot-Check Measurement (0x2A5E) parser scope.** The mandatory byte layout is: **[0]** Flags (uint8, 1 byte), **[1–2]** SpO2 SFLOAT (%), **[3–4]** pulse rate SFLOAT (bpm). A minimum valid frame is therefore 5 bytes. Optional fields — Timestamp (7 bytes, flags bit 0), Measurement Status (3 bytes, flags bit 1), Device and Sensor Status (5 bytes, flags bit 2), Pulse Amplitude Index (SFLOAT 2 bytes, flags bit 3) — start at byte [5] and are not captured. The Flags field is **1 byte (uint8)**, not 2; this is distinct from the Blood Pressure Measurement characteristic where a 1-byte Flags field also precedes the mandatory SFLOAT fields, and from some GATT tutorials that incorrectly widen the PLX Flags to uint16. The bench-validation device (Nonin 3230) implements the SIG-standard layout; `parseSpO2()` has been validated against it.
-
-**Weight — uint16 fixed-point.** The Weight Measurement characteristic (0x2A9D) encodes the weight as a plain **uint16**, not SFLOAT. In SI mode the raw value has a resolution of 0.005 kg (5 g) — divide by 200 to get kilograms. In Imperial mode the resolution is 0.01 lb. The sentinel for an unsuccessful measurement is 0xFFFF. `parseWeightKg()` reads the uint16 directly, applies the fixed-resolution multiplier, and converts lbs to kg when the Imperial flag (bit 0 of the Flags byte) is set.
-
-**Heart rate — plain uint8/uint16.** The Heart Rate Measurement characteristic (0x2A37) carries the heart rate value as a plain unsigned integer — uint8 when flags bit 0 is 0 (the common case for consumer bands), uint16 when bit 0 is 1. No SFLOAT decoding is involved.
-
-### 7.5 Event payload design
-
-Each Notefile uses a [template](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) that stores Notes as fixed-length binary records on the Notecard rather than free-form JSON, reducing per-reading wire size by 3–5×.
-
-`weight.qo` sample:
-```json
-{
- "weight_kg": 84.35,
- "prev_kg": 82.10
-}
-```
-
-`bp.qo` sample:
-```json
-{
- "systolic_mmhg": 138,
- "diastolic_mmhg": 86,
- "pulse_bpm": 74
-}
-```
-
-`spo2.qo` sample:
-```json
-{
- "spo2_pct": 96,
- "pulse_bpm": 71
-}
-```
-
-`activity.qo` sample:
-```json
-{
- "heart_rate_bpm": 68
-}
-```
-
-`vitals_alert.qo` sample (untemplated, immediate sync):
-```json
-{
- "alert": "weight_gain",
- "weight_kg": 84.35,
- "delta_kg": 2.25
-}
-```
-
-Alert types: `weight_gain`, `bp_high`, `spo2_low`, `hr_high`, `hr_low`. The Notecard's metadata envelope (timestamp and device identity) is added automatically and appears alongside the body in Notehub. Location is included only if the device has a configured location source; for a wall-powered indoor hub with no GNSS, the location field will not be present.
-
-### 7.6 Sync strategy and power
-
-**Wall-powered operation — continuous availability.** The hub is powered by USB-C (no battery), so the BLE scanner remains active 24/7. This is critical for the clinical workflow: a patient may take a reading at any time (3 AM weight check, midnight BP, etc.) and the hub must be online to capture it immediately. There is no sleep/wake cycle, no polling intervals; measurements are event-driven and go to Notehub without delay (subject to the 15-minute outbound window, or immediately if they trip a threshold).
-
-**Notecard cellular sync.** The Notecard runs in `periodic` mode with `outbound:15` (minutes) and `inbound:60`. Readings queue on the Notecard between cellular sessions and upload in a single burst every 15 minutes. When a reading trips a threshold, `sync:true` is added to both the measurement Note (e.g., `bp.qo`) and the companion `vitals_alert.qo` Note — both bypass the queue and are typically delivered in the same immediate cellular session, or in back-to-back immediate sessions. The 15-minute default is conservative: for most RPM programs, readings uploading within 15 minutes of being taken is well inside the care-team's response window.
-
-**Alert cooldown.** Once a `vitals_alert.qo` Note fires for a given alert type, the firmware suppresses further alert Notes of the same type for `ALERT_COOLDOWN_MS` (5 minutes). This bounds the volume of notifications the care team receives while a patient remains out of range. The threshold-tripping measurement Note still carries `sync:true` and arrives immediately (the care team's data record is complete), but the companion alert Note (which may trigger a page or automated response) is held back. A reading that remains out of range across multiple consecutive measurements therefore produces at most one alert every 5 minutes rather than one per reading cycle, reducing alert fatigue. The 5-minute floor is a firmware constant in this POC; a production deployment would expose it as a configurable Notehub environment variable alongside the clinical thresholds.
-
-**Radio duty cycle.** The nRF52840 SoftDevice duty-cycles the BLE radio automatically: the scanner runs at a 50% duty cycle (100 milliseconds window every 200 milliseconds interval). The BLE scanner dominates the continuous baseline current draw (~5–10 mA); each cellular session dominates the short peak-current burst (~250 mA) and contributes a significant share of total energy per 15-minute sync window despite being brief (~15–30 seconds).
-
-### 7.7 Error handling
-
-- **`hub.set` with retry and response check.** `notecardConfigure()` guards against an empty `PRODUCT_UID` before sending, retries for up to 5 seconds to paper over the cold-boot I²C race, and inspects the response `err` field via `sendChecked()`. A failed or rejected configuration is logged rather than silently swallowed.
-- **Checked delivery with retry for all Notes.** Both alert Notes and routine measurement Notes use checked enqueue via `sendVitalNoteChecked()` / `sendAlertNote()`, which call `requestAndResponse()` and retry up to `ALERT_ENQUEUE_RETRIES` (3) times on I²C failure, so a transient I²C error cannot silently drop any patient reading. Routine readings call `sendVitalNoteChecked()` with `addSync=false` (normal outbound cadence applies); threshold-tripping readings call it with `addSync=true` to trigger an immediate outbound session for that measurement regardless of alert-cooldown state. The companion `vitals_alert.qo` Note is submitted immediately after via `sendAlertNote()`, also checked and retried. A final failure on a measurement Note is logged; a final failure on an alert Note additionally warns that the alert may be lost.
-- **Configuration calls checked.** `hub.set` and `note.template` calls use `sendChecked()` so a rejected template registration is logged rather than silently swallowed.
-- **Physiological plausibility.** Data callbacks in `ble_central.cpp` apply plausibility guards (defined in `ble_parsers.h`) before setting a reading's `valid` flag. Frames outside human-survival bounds — impossible BP relationships, SpO2 values below 50%, HR outside 20–300 bpm, weight outside 1–500 kg — are logged and discarded. This prevents malformed frames or noncompliant devices from producing impossible readings or triggering spurious alerts.
-- **GATT payload guards.** BLE callbacks check minimum `len` before dereferencing any byte. Measurement-unsuccessful sentinels (0xFFFF for weight; NaN/reserved SFLOAT for blood pressure and SpO2) return −1.0 f or NAN from the parsing helpers; callbacks discard non-positive values.
-- **Env-var validation.** `fetchEnvVars()` checks for a NULL response, a non-empty `err` field, and a non-empty string before calling `atof()`. Related threshold pairs (systolic/diastolic, hr_high/hr_low) are cross-validated for sensible ordering before being applied.
-
-### 7.8 Key code snippet 1: Notecard periodic sync with 15-minute outbound
-
-`hub.set` is sent via a checked retry loop that guards the empty-`PRODUCT_UID` case, handles the cold-boot I²C race, and inspects the Notecard's response `err` field:
-
-```cpp
-bool ok = false;
-const uint32_t t0 = millis();
-do {
- J *req = notecard.newRequest("hub.set");
- JAddStringToObject(req, "product", PRODUCT_UID);
- JAddStringToObject(req, "mode", "periodic");
- JAddNumberToObject(req, "outbound", 15); // upload readings every 15 min
- JAddNumberToObject(req, "inbound", 60); // fetch env vars every 60 min
- ok = sendChecked(req); // checks err field in response
- if (!ok) delay(500);
-} while (!ok && (millis() - t0) < 5000UL);
-```
-
-### 7.9 Key code snippet 2: immediate-sync alert on threshold trip
-
-`sync:true` tells the Notecard to open a session immediately rather than waiting for the next scheduled outbound window. This is essential for a care team that needs to act on a dangerous SpO2 within minutes, not the next quarter-hour.
-
-```cpp
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "vitals_alert.qo");
-JAddBoolToObject (req, "sync", true);
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "spo2_low");
-JAddNumberToObject(body, "spo2_pct", spo2_pct);
-notecard.sendRequest(req);
-```
-
-### 7.10 Key code snippet 3: BLE service discovery and indication subscribe
-
-Called from `bleConnectCallback` when a device connects. The service `discover()` call walks the connected peripheral's ATT database; on a match, `enableIndicate()` writes the CCCD (Client Characteristic Configuration Descriptor) to turn on indications.
-
-```cpp
-if (g_bpSvc.discover(connHandle)) {
- if (g_bpChar.discover()) {
- // Blood Pressure Measurement uses Indication (device sends, hub ACKs)
- g_bpChar.enableIndicate(bpDataCallback);
- }
-}
-```
-
-### 7.11 Key code snippet 4: SFLOAT decode in ble_parsers.h
-
-The IEEE 11073 SFLOAT type is a 16-bit packed float used for blood pressure and SpO2 fields. `parseSfloat()` sign-extends both the 4-bit exponent and the 12-bit mantissa, which is the step most BLE tutorial snippets handle correctly. The less obvious part is sentinel rejection: reserved SFLOAT values are specific **full 16-bit encodings** and must be compared against the raw value before any decoding. Checking only the masked 12-bit mantissa field would incorrectly reject valid numbers whose lower 12 bits match a sentinel pattern but whose exponent is nonzero. For example, `0x17FF` has mantissa bits `0x7FF` that match the NaN sentinel `0x07FF`, but its exponent `0x1` makes it a valid number (mantissa=2047, exponent=1, value=20470). Weight and heart rate do not use this decoder. See §7.4 for their respective encoding formats.
-
-```cpp
-static inline float parseSfloat(uint16_t raw) {
- // Compare the full 16-bit raw value against each reserved sentinel before
- // decoding mantissa/exponent. Checking only the masked 12-bit mantissa
- // field would incorrectly reject valid encodings whose mantissa bits happen
- // to match a sentinel pattern but whose exponent is nonzero (e.g. 0x17FF
- // is a valid number with exponent=1, mantissa=2047, value=20470).
- if (raw == 0x07FFu || raw == 0x0800u ||
- raw == 0x07FEu || raw == 0x0802u ||
- raw == 0x0801u) {
- return NAN;
- }
- // Sign-extend mantissa: bits [11:0] → int16
- int16_t mantissa = (int16_t)(raw & 0x0FFFu);
- if (mantissa & 0x0800) {
- mantissa |= (int16_t)0xF000; // propagate sign bit
- }
- // Exponent: sign-extend bits [15:12] to a full int8
- int8_t exponent = (int8_t)((raw >> 12) & 0x0F);
- if (exponent & 0x08) {
- exponent |= (int8_t)0xF0; // propagate sign bit
- }
- return (float)mantissa * powf(10.0f, (float)exponent);
-}
-```
-
-
-## 8. Data Flow
-
-
-
-**Collected.** Every time a patient uses a BLE health device: weight (kg), blood pressure (systolic/diastolic mmHg and pulse bpm), SpO2 (percent) and pulse rate, heart rate (bpm from the activity band). The hub does not poll on a fixed schedule — readings are event-driven, captured when the device transmits.
-
-**Transmitted.**
-- `weight.qo`, `bp.qo`, `spo2.qo`, `activity.qo` — queued when a reading arrives and uploaded in the next 15-minute outbound sync window, **unless** the reading trips a threshold, in which case `sync:true` is also added to the measurement Note and it uploads immediately. Template-encoded; each Note is a compact binary record on the Notecard.
-- `vitals_alert.qo` — emitted alongside every threshold-tripping measurement Note, also with `sync:true`. Both the measurement Note and the alert Note typically arrive in the same immediate cellular session (~15–60 s after the reading), or in back-to-back immediate sessions — not on the next scheduled sync.
-
-**Routed.** All five Notefiles land in Notehub and from there to whatever downstream the project's routes specify. The recommended split: reading Notefiles → long-term analytics or EHR staging; `vitals_alert.qo` → on-call or care coordinator notification channel (SMS gateway, webhook, CMMS ticket, etc.).
-
-**Alerts trigger on:**
-- `weight_gain` — current weight reading exceeds the previous weight reading in the same boot session by ≥ `weight_delta_kg`. Body: `alert`, `weight_kg`, `delta_kg`. (No alert on the first reading of a session; delta state does not persist across power cycles — see Limitations.)
-- `bp_high` — systolic ≥ `bp_systolic_high` or diastolic ≥ `bp_diastolic_high`. Body: `alert`, `systolic_mmhg`, `diastolic_mmhg`.
-- `spo2_low` — SpO2 < `spo2_low`. Body: `alert`, `spo2_pct`.
-- `hr_high` — heart rate from the activity band (Heart Rate Service, 0x180D) > `hr_high`. Body: `alert`, `heart_rate_bpm`. Note: pulse values from the BP cuff or pulse oximeter are not evaluated against this threshold.
-- `hr_low` — heart rate from the activity band (Heart Rate Service, 0x180D) < `hr_low` and > 0. Body: `alert`, `heart_rate_bpm`. Same scope: activity band only.
-
-**Alert deduplication.** When a reading trips a threshold the measurement Note always carries `sync:true` and uploads immediately. The companion `vitals_alert.qo` Note is additionally rate-limited by `ALERT_COOLDOWN_MS` (5 minutes per alert type): if the same alert type fired within the last 5 minutes, the alert Note is suppressed while the measurement Note still syncs. This bounds the volume of actionable notifications the care team receives while keeping the raw data record complete.
-
-
-## 9. Validation and Testing
-
-**Expected steady-state.** In a normal day a post-discharge patient takes weight once in the morning, BP twice (morning and evening), SpO2 once or twice, and wears the activity band intermittently. Activity band heart rate samples are capped at one per `HR_SAMPLE_INTERVAL_MS` (15 minutes); a patient who wears the band for two hours yields at most 8 HR Notes during that period. Overall the hub produces roughly 5–15 reading Notes per day across the four Notefiles and zero alert Notes in a recovering patient. The first set of Notes should appear in Notehub within 15 minutes of the first measurement after the hub comes online.
-
-**Simulating a threshold trip.** The fastest way to test alert delivery: set `bp_systolic_high` to `100` in the Fleet environment, and any normal BP reading will then trip `bp_high`. After the next inbound sync pulls the new value, take a BP measurement and confirm that both a `bp.qo` Note and a `vitals_alert.qo` Note appear in Notehub within a cellular session-establishment window (typically 15–60 s). Reset the threshold to `160` when done.
-
-**Verifying BLE connectivity.** Open the Arduino serial monitor at 115200 baud (**debug build only** — set `DEBUG_VITALS=1` and `ALLOW_DEBUG_BUILD` per §7.1; the default shipping build emits no serial output). Each scan hit, connection, and characteristic subscription prints a `[BLE]` line; each reading prints a `[VITALS]` line. If no devices appear, confirm the BLE health device is in measurement-ready state (most BLE health devices only advertise immediately after a measurement is initiated) and that it implements the standard Bluetooth SIG service UUID for its category.
-
-**Power validation with Mojo.** Splice the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the USB-C supply and the Notecarrier F's power input. The hub operates in two mutually exclusive states plus a recurrent burst:
-
-| State | Expected draw | Notes |
-|---|---|---|
-| Continuous baseline — host + BLE scanning | ~5–10 mA | nRF52840 SoftDevice running at 50% scan duty cycle (+4 dBm). Notecard idle contribution (~8–18 µA) adds negligibly to this baseline. |
-| Cellular session burst (~15–30 s, once every 15 min) | ~250 mA average; brief spikes to ~500 mA at peak LTE Cat-1 bis TX. The ~2 A figure applies only when operating in GSM mode (2G fallback, select regions) — see the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/). | LTE Cat-1 bis data transfer. Also fires on any threshold-tripping measurement Note or alert Note with `sync:true`. |
-
-The continuous baseline is dominated by the BLE scanner. The cellular burst is brief but contributes a significant share of total energy per sync window: roughly 30 s at ~250 mA (LTE Cat-1 bis) every 15 minutes. Both are well within the range of a standard 5V/2A USB phone charger. Notecard current figures are drawn from the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/) and the [low-power design guide](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/). Note that the ~8–18 µA Notecard idle figure is a module-level datasheet specification; the Mojo measures the whole powered stack at the USB input, which will read higher due to the nRF52840 and the Notecarrier F's regulator. When bench-validating, focus on trace shape and sync-burst cadence rather than expecting the Notecard datasheet idle figure to appear directly at the USB power input.
-
-A healthy Mojo trace for this hub should show: a steady ~5–10 mA baseline (BLE scanning continuous), with a 15–30-second burst at ~250 mA (LTE Cat-1 bis) once every 15 minutes (scheduled cellular sync), and additional ~250 mA bursts whenever a threshold-tripping measurement or alert Note carries `sync:true`. In regions where the network falls back to GSM the cellular burst can spike briefly to ~2 A during transmit; this is normal MBGLW behavior on GSM but should not be seen in LTE Cat-1 bis–only deployments. If cellular bursts are absent or far less frequent than 15 minutes, verify `PRODUCT_UID` is set and the Notecard has coverage (check `_session.qo` events in Notehub).
-
-### Troubleshooting
-
-A short field guide for the issues that actually arise during first bring-up and deployment.
-
-| 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 not routed. | Verify `PRODUCT_UID` exactly matches the Notehub project. Confirm the u.FL antenna pigtail is attached to the Notecard. Check for `_session.qo` events — if none appear, there is no cellular connection. |
-| `_session.qo` events arrive but no reading Notes ever appear. | No BLE health device within range, or device is not advertising its standard service UUID. | Open the serial monitor at 115200 baud and watch for `[BLE] Scanner started` (**debug build only** — see §7.1 build flags). Bring a BLE health device within a few meters and initiate a measurement — most devices only advertise during an active measurement, not continuously. Confirm the device uses the standard Bluetooth SIG service UUID (not a proprietary profile). |
-| BLE device connects but the reading struct is never populated. | Device sends a measurement-unsuccessful sentinel (0xFFFF for weight; NaN or reserved SFLOAT for blood pressure or SpO2), or the characteristic byte layout differs from the SIG specification. | Check `[BLE]` log lines (**debug build only** — see §7.1 build flags) for the service discovery step. If discovery succeeds but the data callback never sets `valid`, add `Serial.printf` in the callback to print the raw bytes and compare against the device's BLE specification or use a BLE sniffer app (nRF Connect for Mobile works well). |
-| Alerts fire on every reading even though values look normal. | A clinically too-tight threshold value, stale cached env values, or a scope-override shadowing the intended value. | In Notehub, verify each threshold env var holds a sensible value for the patient population. Out-of-range and non-numeric values (including `"0"` and blank strings) are rejected in firmware by `validateEnvFloat` — each var has explicit bounds (e.g. `bp_systolic_high` ∈ [60, 260], `hr_low` ∈ [20, 100], `weight_delta_kg` ∈ [0.5, 20]); a rejected value falls back to the previously accepted threshold rather than being applied. Notehub applies env vars hierarchically: project scope is the baseline, fleet scope overrides it, and device scope overrides both. A device-level value silently shadows a fleet or project default, so check all three scopes when a threshold behaves unexpectedly. |
-| Weight readings are off by a factor of ~2.2. | Device is transmitting in Imperial (lbs) but the flags byte isn't being parsed correctly. | The Flags byte (byte 0) bit 0 determines SI vs. Imperial. Log `data[0]` in `weightDataCallback` and verify. If the device always sends Imperial, the `parseWeightKg` helper already converts lbs → kg when the flag is set. |
-| No cellular sync for more than 15 minutes. | Notecard in coverage gap, or firmware stuck before calling `submitWeight / submitBp / etc.` | Run `hub.status` from the blues.dev In-Browser Terminal — it returns current signal strength and sync state. If coverage is good but syncs are absent, confirm the main `loop()` is running (add a serial heartbeat print every 30 seconds). |
-| Mojo bench trace shows no cellular bursts at all. | `hub.set` was not applied, or Notecard is in `minimum` mode from a prior configuration. | Issue `{"req":"hub.set","mode":"periodic","outbound":15,"inbound":60}` from the blues.dev In-Browser Terminal to reset sync mode. Power-cycle the hub; the sketch calls `notecardConfigure()` in `setup()` which re-applies the configuration. |
-
-If a problem is not on this list, the [Blues community forum](https://discuss.blues.com) is the fastest place to get a second pair of eyes on a Notecard + BLE setup.
-
-
-## 10. Limitations and Next Steps
-
-
-
-**Reminder:** this is a proof-of-concept reference design, not a cleared medical device. It is not intended for diagnostic, emergency, or life-sustaining use. Any production deployment must address PHI/HIPAA compliance, secure data routing and storage, auditability, data retention policies, and applicable regulatory review before handling real patient data.
-
-
-
-The hub is intentionally scoped to the post-discharge window: one patient, one wall outlet, the standard Bluetooth SIG health profiles, and the smallest viable commissioning workflow. Per-patient baselines, multi-device concurrency, OTA firmware, and proprietary activity-band data are all real product features that belong in a follow-on rather than complicating the reference design.
-
-### Simplified for the POC
-
-The simplifications below are deliberate scope choices. Each is a place where a production deployment will add concurrency, a configurable knob, or a hardened security path once a real post-discharge program runs it. As the warning above states, **this is a proof-of-concept reference design, not a cleared medical device.**
-
-**One BLE connection at a time.** The nRF52840's Central role supports multiple concurrent connections, but the sketch only opens one. If a patient manages to trigger two devices simultaneously (unlikely with health devices that connect-measure-disconnect quickly), the second connect attempt is queued after the first device disconnects. A multi-connection extension is straightforward in the Adafruit nRF52 library.
-
-**Alert cooldown is a fixed firmware constant, not remotely configurable.** The hub suppresses repeated `vitals_alert.qo` Notes of the same type for 5 minutes (`ALERT_COOLDOWN_MS`). The cooldown duration cannot be adjusted per patient without a firmware reflash. A production deployment would expose this as a Notehub environment variable alongside the clinical thresholds, so care coordinators can tighten or relax the alert rate for individual patients.
-
-**Weight-delta alerting, not absolute.** The `weight_gain` alert fires on change vs. previous reading, not on absolute weight. On first boot there is no previous reading so no delta alert is possible. A daily-comparison window (today vs. same time yesterday) is a more robust clinical metric and requires persisting the previous day's reading — doable by storing it in a `_notecard.db` database Notefile.
-
-**Heart rate only from activity band.** The Heart Rate Service (0x180D) provides heart rate, but step count is not part of the standard service; it lives in proprietary manufacturer-specific GATT services that differ across activity band brands. The firmware captures only heart rate from that service.
-
-**Blood Pressure Measurement — `user_id` and `measurement_status` not captured.** `parseBpMmhg` correctly decodes systolic, diastolic, and pulse rate for any compliant 0x2A35 layout. Only the timestamp field (flags bit 1) affects the pulse-rate offset: the parser adds 7 bytes to the mandatory 7-byte header when the timestamp is present, correctly handling both timestamp-present (Omron M4) and timestamp-absent layouts. The User ID (flags bit 3) and Measurement Status (flags bit 4) fields appear after pulse rate in the spec ordering; they are not used and their values are discarded.
-
-**BLE bonding requires a commissioning step, and public/static-address devices also require an allow-list entry.** The hub is **fail-closed by default: unrecognized devices are ignored.** Before shipping to a patient, a commissioning build with both `-DALLOW_UNENROLLED_DEVICES_FOR_DEV=1` and `-DALLOW_COMMISSIONING_BUILD` is needed to pair the patient's specific devices and store their bond keys. After commissioning, inspect the `bond_established` Notes in `commissioning.db` on Notehub: any device whose `addr_type` is `0x00` (public) or `0x01` (random static) must also be added to `ENROLLED_DEVICES` in `vitals_config.h`. Bonding alone is not sufficient for those devices because the hub cannot identify them through IRK resolution. **Remove both commissioning flags before deploying to patients.** The MAC allow-list is a compile-time constant that cannot be updated without a reflash.
-
-**BLE pairing uses encrypted but unauthenticated bonding.** The commissioning step uses Just Works pairing (`setMITMProtect(false)`) because most consumer health devices lack input/output capabilities. This provides an encrypted BLE connection but **offers no man-in-the-middle protection during the initial key exchange.** For production deployments handling patient data, the clinical and security teams should define a controlled commissioning procedure (e.g., pairing only in a secured kitting environment at the point of device preparation), evaluate device and vendor choices that support authenticated pairing modes (Numeric Comparison or Passkey Entry) where input/output capabilities exist, and review the full threat model for the deployment setting before processing real patient data.
-
-**No offline-reading catchup.** If the hub is unplugged for several days and re-plugged, it doesn't know what readings it missed. Readings taken while the hub was offline are simply absent from the record.
-
-**Activity band steps not captured.** Standard Heart Rate Service (0x180D) does not carry step count. Activity bands that expose a proprietary step-count characteristic require a device-specific firmware extension.
-
-### Production Next Steps
-
-Taking this proof-of-concept toward a fieldable hub means hardening firmware delivery, baseline modeling, and supply-rail visibility for a real patient population.
-
-**Investigate host firmware OTA.** [Notecard Outboard Firmware Update](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) targets hosts whose bootloader can be driven over UART by the Notecard (e.g., STM32 System Memory bootloader). The Adafruit Feather nRF52840's UF2 bootloader uses USB mass-storage enumeration, which does not fit that model. A production deployment would need either a custom UART-accessible bootloader on the nRF52840 or an alternative delivery path (for example, Nordic Bluetooth DFU triggered by a Notecard-delivered environment-variable flag) before OTA firmware updates can be offered as a supported feature.
-
-**Per-patient baseline learning.** Store a 7-day rolling average weight in a local `.db` Notefile and alert when the single-day reading is more than `weight_delta_kg` above the rolling average rather than just the previous reading.
-
-**Heartbeat Note.** Emit a `heartbeat.qo` every 24 hours even if no readings arrived, so the care team can distinguish "patient not using devices" from "hub offline."
-
-**Low-voltage notification.** The Notecard's `card.voltage` response monitors the supply rail; a `voltage_low` alert can page the care coordinator if the hub is losing power or the USB supply is marginal.
-
-**Remotely configurable alert cooldown.** Expose `ALERT_COOLDOWN_MS` as a Notehub environment variable so care coordinators can tune the alert rate per patient without a reflash.
-
-
-## 11. Summary
-
-The 82-year-old heart failure patient sets the hub on the nightstand and forgets it exists. The discharge coordinator who mailed it sees weight, blood pressure, SpO2, and heart rate land in Notehub the first time a device is used (no password, no app, no support call) and gets paged within minutes if any reading crosses a clinical threshold. A Feather nRF52840 talking to the BLE health-device profiles on one side and a cellular Notecard on the other does the work; care coordinators retune per-patient thresholds from Notehub environment variables without a firmware change or a truck roll. For the patients who don't have home WiFi, can't configure a tablet, or are recovering at a family member's house, this is the only hub architecture that reliably delivers the clinical outcome the program was built to produce.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/post-discharge-vitals-relay-hub/)**
diff --git a/87-post-discharge-vitals-relay-hub/banner.png b/87-post-discharge-vitals-relay-hub/banner.png
deleted file mode 100644
index faaa84ab..00000000
Binary files a/87-post-discharge-vitals-relay-hub/banner.png and /dev/null differ
diff --git a/87-post-discharge-vitals-relay-hub/diagrams/01-system-architecture.svg b/87-post-discharge-vitals-relay-hub/diagrams/01-system-architecture.svg
deleted file mode 100644
index 22b57c67..00000000
--- a/87-post-discharge-vitals-relay-hub/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,79 +0,0 @@
-
diff --git a/87-post-discharge-vitals-relay-hub/diagrams/02-wiring-assembly.svg b/87-post-discharge-vitals-relay-hub/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index 99a3a743..00000000
--- a/87-post-discharge-vitals-relay-hub/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,101 +0,0 @@
-
diff --git a/87-post-discharge-vitals-relay-hub/diagrams/03-data-flow.svg b/87-post-discharge-vitals-relay-hub/diagrams/03-data-flow.svg
deleted file mode 100644
index fa993696..00000000
--- a/87-post-discharge-vitals-relay-hub/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,102 +0,0 @@
-
diff --git a/88-reefer-trailer-cold-chain-door-event-monitor/README.md b/88-reefer-trailer-cold-chain-door-event-monitor/README.md
index e7daaf13..70d5ae96 100644
--- a/88-reefer-trailer-cold-chain-door-event-monitor/README.md
+++ b/88-reefer-trailer-cold-chain-door-event-monitor/README.md
@@ -1,544 +1,5 @@
# Reefer Trailer Cold-Chain & Door-Event Monitor
-
+A cellular and satellite reference design that keeps continuous watch over refrigerated trailers, catching temperature excursions and logging door events to protect cold-chain cargo.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Reefer%20Trailer%20Cold-Chain%20%26%20Door-Event%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/reefer_cold_chain_monitor/](firmware/reefer_cold_chain_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a [loss prevention](https://blues.com/loss-prevention/) reference design that keeps continuous watch over refrigerated (**reefer**) trailers, catching temperature excursions before a load is spoiled and logging every door event before a pallet walks out the back. Two DS18B20 temperature probes and a magnetic door reed switch feed a Blues Notecarrier CX, which packages sensor events and hands them to a Notecard for [Skylo](https://www.skylo.tech/resources/geographical-coverage) for multi-network delivery: cellular when a tower is in range, Skylo satellite when it isn't.
-
-**What you'll have when you're done:** a weatherproof, trailer-powered monitor that samples cargo temperature every minute, queues each sample locally for [Blues Notehub](https://blues.com/notehub/) cloud service delivery on the next outbound session (arriving in the cloud/Notehub in batches, default once per hour), fires an alert on the next sample cycle (up to ~60 seconds after the event, plus network-establishment time) when a door opens or a temperature threshold is breached, and ships hourly compliance summaries to Notehub over cellular or WiFi. Per-sample logs and hourly summaries use cellular/WiFi-only Notefiles; the Notecard discards their queued Notes when connecting via NTN, so they never consume the bundled satellite data budget. On periodic hub sessions over NTN — which occur when terrestrial coverage is unavailable — only pending alert Notes carry payload data over the satellite link.
-
-**Energy footprint:** steady-state draw from a 12 V trailer supply is roughly 30–60 mAh per 24 h (dominated by one hourly cellular session of ~15–45 seconds at ~250 mA average). NTN satellite sessions consume more per event due to longer link establishment, but alert-only delivery over satellite keeps the 10 KB data budget intact. Commissioning validates actual consumption with [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link). See [§9](#9-validation-and-testing).
-
-
-## 1. Project Overview
-
-**The problem.** A refrigerated trailer hauling fresh produce, pharmaceuticals, or frozen food is one of the higher-value assets in ground transportation — a typical load runs $40,000 to $80,000 before you factor in the trailer itself. Two failure modes create most of the loss:
-
-- **Temperature excursion.** A reefer unit that loses refrigerant, trips a setpoint, or runs out of fuel can warm a frozen or fresh cargo hold into the danger zone. The spoilage is often discovered only at delivery, too late to re-route the load. Catching the excursion within the first hour, while options still exist, is the difference between a salvaged load and a write-off.
-- **Door theft.** Drop-stop cargo theft is systematic: a thief follows a trailer to a fuel stop or rest area, opens the rear doors, and removes full pallets in minutes. The most reliable early indicator is a door opening at an unexpected location or time.
-
-Both failure modes have a common root: nobody can see inside the trailer while it's moving.
-
-**Scope note.** This design uses direct temperature-probe and door-switch input signals only — no OEM reefer unit or tractor integration, no J1939 CAN, no proprietary reefer telemetry (Carrier DataLink, Thermo King DSR, etc.). It is a point-instrumentation retrofit suitable for fleets with heterogeneous reefer makes and models where a universal, non-invasive monitor is preferred.
-
-**Why Notecard for Skylo.** A trailer is mobile by definition, and its connectivity environment changes constantly. Urban routes have dense cellular coverage. Rural interstates may have significant gaps. Cross-border hauls hand off between carrier networks. Ocean-facing port staging areas can be outside terrestrial coverage entirely. A fixed cellular SIM solves the urban case but silently drops offline everywhere else, exactly when unsupervised, high-value loads are most exposed.
-
-
-
-[Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link). See the [datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) — addresses this in a single 30 × 42 mm module: LTE-M, NB-IoT, and GPRS for terrestrial cellular, Skylo **NTN** (non-terrestrial network) satellite as an automatic fallback, and WiFi via an onboard antenna. The firmware enables this multi-RAT behavior with a single one-time [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) request (`method:"wifi-cell-ntn"`); from then on the Notecard manages network selection itself, and the application never needs to know which radio carried a given message. An alert triggered by a door opening at a rural rest stop will use cellular if a tower is within range and satellite if it isn't, with no radio-selection logic required in the application. For a mobile asset that can't self-select its coverage environment, this multi-RAT (radio access technology) automatic failover is the whole value proposition.
-
-**Deployment scenario.** The electronics mount in a weatherproof NEMA 4X enclosure strapped or screwed to the interior trailer wall or exterior chassis rail, powered from the trailer's 12 V DC supply (or the nose-box connector if available). Two DS18B20 stainless steel probes thread through grommets into the cargo compartment — one near the front (forward air) and one near the rear return-air panel, and the reed switch mounts on the door frame with its matching magnet on the door itself.
-
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Down the road at 65 mph, the Cygnet STM32 host on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) is off — power-gated by [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn). Every 60 seconds it wakes long enough to read both DS18B20 probes and the door pin, evaluate the threshold rules locally, and hand any resulting Notes to the Notecard over I²C. Because the host loses power between samples, the accumulating state (running temperature averages, the door open/close timestamp, the alert deduplication epoch) is serialized into Notecard flash by `NotePayloadSaveAndSleep` and restored on the next wake via `NotePayloadRetrieveAfterSleep`. Nothing is lost across sleep cycles, and the trailer's 12 V supply sees only a few seconds of MCU draw per minute.
-
-**Notecard responsibilities.** Notecard for Skylo handles every part of "getting the message off the trailer." It queues [Notes](https://dev.blues.io/api-reference/glossary/#note) locally, runs the WiFi → cellular → NTN fallback policy that the firmware sets up once at first boot via [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) (`method:"wifi-cell-ntn"`), opens a session on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence (default 60 minutes), and flushes pending alerts immediately when the host calls [`hub.sync`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-sync) over whatever network happens to be available. It pulls [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) down from Notehub so dispatch can retune thresholds without reflashing. And because Notecard for Skylo has an integrated GNSS radio, the firmware turns on [`card.location.mode`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location-mode) `periodic` (600-second interval, motion-gated per the API docs) so every `trailer_alert.qo` carries the last known latitude and longitude, answering "where was this trailer when the alert fired?" without any extra hardware.
-
-**Notehub responsibilities.** Whatever the Notecard sends — cellular, WiFi, or satellite — lands in [Notehub](https://notehub.io), which ingests, stores, and applies project [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). Alerts, per-sample logs, and hourly summaries live in separate [Notefiles](https://dev.blues.io/api-reference/glossary/#notefile), so dispatch routes can take immediate alerts to the TMS or on-call channel while the per-sample and summary streams flow into a long-term cold-chain compliance store. The `trailer_alert.qo` payload format is compact+port-encoded so it works identically over cellular and NTN, and Notehub session metadata records which transport carried each event. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) group trailers by lane, customer, or cargo type and push fleet-specific threshold overrides without touching individual device configs.
-
-**Routing to the cloud (high level).** Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations; route setup is project-specific. See the [Notehub routing docs](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub); this project ships no specific downstream endpoint.
-
-
-## 3. Technical Summary
-
-1. **Notehub:** create a [Notehub project](https://notehub.io) and copy its ProductUID.
-2. **Wire the bench rig:** Notecarrier CX + Notecard for Skylo + two DS18B20 probes (VDD to +3V3, GND to GND, data to A0 with 4.7 kΩ pull-up to +3V3) + reed switch on A1. Full pinout in [§5](#5-wiring-and-assembly).
-3. **Edit one line** of [`firmware/reefer_cold_chain_monitor/reefer_cold_chain_monitor_helpers.h`](firmware/reefer_cold_chain_monitor/reefer_cold_chain_monitor_helpers.h): set `PRODUCT_UID` to your project's value.
-4. **Flash with arduino-cli**:
- ```bash
- # Install libraries: Notecard, OneWire, DallasTemperature (via Arduino Library Manager)
- # Find your Cygnet FQBN
- arduino-cli board listall | grep -i cygnet
- # Output will resemble: Cygnet STMicroelectronics:stm32:Blues:pnum=CYGNET
-
- # Compile and upload (substitute your FQBN and port)
- arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/reefer_cold_chain_monitor/
- arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbmodem* firmware/reefer_cold_chain_monitor/
- ```
- See [§7.1](#71-installing-and-flashing) for full dependency list and IDE steps.
-5. **Watch:** open Notehub → your project → **Events**. You should see `_session.qo` within 1 minute, then on the next outbound window (default ~60 minutes): a batch of `trailer_log_cell.qo` Notes (one per 60-second sample), `trailer_summary_cell.qo` (hourly), and any alerts as `trailer_alert.qo` (immediate sync).
-
-Here is a sample Note this device emits:
-```json
-{ "t1_c": -1.4, "t2_c": -0.8, "door_open": false }
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32 host — no separate MCU board needed. Supports `card.attn` power gating for deep-sleep between samples. See the [datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/). |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Cellular (LTE-M / NB-IoT / GPRS) + Skylo NTN satellite module with built-in GNSS and onboard WiFi antenna (Quectel BG95-S5 cellular/satellite modem + Silicon Labs WFM200S WiFi). Automatic network selection requires no firmware changes. Bundles 500 MB cellular data + 10 KB satellite data over 10 years with no activation fees. See the [datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/). |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter on the power rail. Required during commissioning and validation for ground-truth energy measurement (see [§9](#9-validation-and-testing)). Remove Mojo and the Qwiic cable before production deployment — the inline current draw and quiescent load are not needed in the field. See the [datasheet](https://dev.blues.io/datasheets/mojo-datasheet/). |
-| [Adafruit DS18B20 Waterproof Temperature Sensor](https://www.adafruit.com/product/381) (#381) | 2 | 1-Wire stainless-steel probe; −55 °C to +125 °C range; ±0.5 °C from −10 °C to +85 °C; includes a 4.7 kΩ pull-up resistor. One probe near the front evaporator; one near the rear return-air panel. |
-| [Adafruit Magnetic Contact Switch](https://www.adafruit.com/product/375) (#375) | 1 | Normally-open reed switch in a plastic housing. Activates within 15 mm of its matching magnet. Mounts on the door frame; magnet on the door leaf. |
-| [Pololu D24V22F5 5 V Step-Down Regulator](https://www.pololu.com/product/2858) (#2858) | 1 | 5.3–36 V input, 5 V / 2.5 A output. Converts trailer 12 V DC (or nose-box 12 V) to 5 V for the Notecarrier CX. Compact and efficient (85–95%). |
-| NEMA 4X enclosure, ~6 × 4 × 2 in | 1 | Weatherproof housing rated for wash-down and condensation — appropriate for the interior of a reefer trailer. |
-| Skylo-certified cellular / NTN flexible antenna — **included with NOTE-NBGLWX** | 1 | Ships in the NOTE-NBGLWX kit; connects to the Notecard `MAIN` u.FL port. Covers LTE-M / NB-IoT / GPRS cellular and Skylo S-Band / L-Band NTN (B23, B255, B256). **Must not be substituted.** Using any other antenna on the `MAIN` port decertifies the device on Skylo's network and may result in the device being blocked. If a different antenna is required for a production design, a delta EIRP test report from a CTIA/OTA-authorized lab is needed. See the [Blues Antenna Guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/) for the full certification policy. Route the included flexible antenna cable through a sealed IP68 cable gland in the NEMA 4X enclosure wall so the antenna sits outside the metal structure. |
-| Passive GPS/GNSS flexible antenna — **included with NOTE-NBGLWX** | 1 | Ships in the NOTE-NBGLWX kit; connects to the Notecard `GPS` u.FL port. Covers GPS/GNSS L1 (1559–1610 MHz). Route the cable through a sealed cable gland to the exterior of the NEMA 4X enclosure with a clear, unobstructed sky view — mount the antenna on the trailer roof or exterior wall, away from the refrigeration unit. A replacement u.FL passive GNSS antenna (e.g., the [Blues accessories Quectel YCA001BA](https://shop.blues.com/collections/accessories), covering 1560–1620 MHz, u.FL) is compatible if the included antenna is damaged. |
-| Sealed IP68 cable gland, M16, for the antenna cables | 2 | One per antenna cable (MAIN and GPS), routed through the NEMA 4X enclosure wall. Maintains the IP/NEMA rating after the antenna cables are passed through. Use a gland sized for the cable diameter of the included antenna pigtails (typically ~3–4 mm). |
-| 2-conductor stranded wire, 22 AWG, ~3 m | 1 | Extends the door sensor cable from the door frame to the enclosure. Standard alarm wire. |
-| Inline ATO/ATC blade fuse holder, 12–16 AWG leads | 1 | Installs inline on the trailer 12 V positive feed between the trailer source and the Pololu D24V22F5 `VIN`. Keeps overcurrent protection as close to the source as practical, per the field installation caution in [§5](#5-wiring-and-assembly). Any automotive-grade in-line ATO/ATC holder with lead wire gauge matched to the supply run is acceptable. |
-| 3 A ATO/ATC automotive blade fuse | 1 | Fits the inline fuse holder above. Sized for the Pololu D24V22F5's 2.5 A rated output with wiring headroom. Use only 3 A — do not substitute a higher-rated fuse; a blown fuse indicates a wiring fault that must be corrected before replacing it. |
-
-All Blues hardware ships with an active SIM; no separate SIM purchase or activation is required.
-
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. Notecard for Skylo seats into the M.2 slot; cellular, satellite, and GNSS antennas connect via u.FL leads to externally-mounted antennas. If bench-validating with the Mojo, it sits inline between the 5 V supply and the Notecarrier's `+VBAT` pad, reporting cumulative mAh to the Notecard over Qwiic (I²C).
-
-**Power (production, no Mojo):**
-- Pololu D24V22F5 `VIN` → inline fuse holder (3 A ATO blade fuse fitted; see BOM) → trailer 12 V DC positive. Mount the fuse holder on the positive lead as close to the 12 V source as practical. See the field installation caution below.
-- Pololu `GND` → trailer chassis GND (circuit ground reference for the entire system)
-- Pololu `VOUT` (+5 V) → Notecarrier CX `+VBAT`
-- Pololu `GND` → Notecarrier CX `GND` (power-return path; must share the same chassis ground as the input side)
-
-**Power (bench validation, with Mojo inline):**
-- Pololu `VIN` → bench 12 V supply positive
-- Pololu `GND` → bench supply GND (circuit ground reference)
-- Pololu `VOUT` (+5 V) → Mojo `BAT`
-- Mojo `LOAD` → Notecarrier CX `+VBAT`
-- Pololu `GND` → Mojo `GND` → Notecarrier CX `GND` (shared ground throughout the power chain)
-- Mojo Qwiic → Notecarrier CX Qwiic — I²C telemetry connection for bench energy measurement only; remove Mojo and the Qwiic cable in production
-
-**DS18B20 temperature probes (1-Wire bus on A0):**
-- Both probe `VDD` (red) wires → Notecarrier CX `+3V3`
-- Both probe `GND` (black) wires → Notecarrier CX `GND`
-- Both probe data (yellow) wires → Notecarrier CX `A0`
-- Notecarrier CX `+3V3` → 4.7 kΩ resistor (included in Adafruit #381 bag) → Notecarrier CX `A0` (pull-up)
-
-> **Probe order.** `getTempCByIndex(0)` returns whichever probe the library enumerates first at startup. To match index to physical location, power up with only one probe connected, Note its reported temperature, label it, then connect the second. This is a one-time bench step.
-
-**Door reed switch (A1):**
-- Adafruit #375 lead 1 → Notecarrier CX `A1`
-- Adafruit #375 lead 2 → Notecarrier CX `GND`
-- Firmware uses `INPUT_PULLUP` on A1; no external resistor needed.
-
-> **Polarity:** The Adafruit #375 is a normally-open (NO) switch — the circuit is open when no magnet is present. Mount the plastic housing on the **stationary door frame** and the magnet on the **moving door**. When the door closes and the magnet approaches, the circuit closes and pulls `A1` LOW. When the door opens, the circuit opens and `A1` goes HIGH via the pull-up. The firmware interprets `HIGH = door open`.
-
-**Door sensor cable extension:** The #375 ships with a 29 cm (≈ 11 in) pigtail. Splice 22 AWG 2-conductor wire at the sensor housing terminals to run from the door frame to the enclosure, keeping the join inside a weatherproof connector (e.g., a gel-filled butt splice).
-
-**Antennas:**
-- Notecard `MAIN` u.FL → included Skylo-certified flexible antenna cable → through sealed IP68 cable gland in enclosure wall → antenna positioned outside with clear sky view
-- Notecard `GPS` u.FL → included passive GPS flexible antenna cable → through sealed IP68 cable gland in enclosure wall → antenna positioned outside with clear sky view (trailer roof or exterior wall, away from the refrigeration unit)
-
-> **Antenna placement.** Both antennas must be outside the metal enclosure and any steel trailer structure — cellular, NTN, and GNSS signals attenuate severely through steel. Keep the GNSS antenna at least 20 cm from the cellular/NTN antenna and from reefer unit motors or inverters to avoid desensitization. Seat all u.FL connectors firmly before routing cables — they are fragile push-on connectors. Route antenna cables through the IP68 cable glands before sealing; avoid sharp bends and maintain at least 11 mm clearance around each antenna.
->
-> **Skylo antenna substitution.** The `MAIN` port antenna must be the Skylo-certified flexible antenna included with the NOTE-NBGLWX. Connecting any substitute antenna, including a seemingly equivalent cellular/NTN whip, decertifies the device on Skylo's network and Skylo may block it from the NTN service. If a different antenna is required for a custom enclosure or mounting scenario, contact [Blues](https://blues.com/contact-sales/) about the delta EIRP certification process before deployment.
-
-
-
-**Field installation caution.** Tap the 12 V feed from a dedicated trailer accessory circuit and fit a 3 A automotive blade fuse as close to the source as practical — do not wire directly to the battery without overcurrent protection. Route all sensor and power cables away from refrigerant lines, the reefer unit wiring harness, and OEM trailer wiring; do not share conduit with high-current circuits. Avoid penetrating insulated trailer panels where possible — route cables through existing grommets or approved bulkhead fittings to preserve the trailer's thermal envelope. Any non-OEM penetrations must be sealed with closed-cell foam tape and recorded in the trailer maintenance log.
-
-
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) — it looks like `com.your-company.your-name:reefer-monitor`.
-
-2. **Set the ProductUID in firmware.** Open [`reefer_cold_chain_monitor_helpers.h`](firmware/reefer_cold_chain_monitor/reefer_cold_chain_monitor_helpers.h) and replace the empty string on the `#define PRODUCT_UID ""` line with your value.
-
-3. **Claim the Notecard.** Power the assembled unit. On first cellular (or satellite) connect the Notecard associates with your Notehub project automatically — the device appears in your project's **Devices** tab within a minute or two.
-
-4. **Create a Fleet per lane or customer.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration and routing. A natural division here is one fleet per cargo type (fresh, frozen, pharmaceutical) since temperature thresholds differ significantly between them. [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can auto-assign trailers based on properties already in Notehub.
-
-5. **Set environment variables.** In Notehub web console: select your Fleet (or individual Device) → **Settings** → **Environment** tab. Type each variable name and value below. Changes take effect on the next inbound sync (default 120 minutes) — no reflash, no truck roll required.
-
- | Variable | Default | Valid range | Purpose |
- |---|---|---|---|
- | `temp_max_c` | `7.0` | −30.0 to 50.0 °C | Warm excursion threshold in °C. An alert with `alert:"temp_excursion"` fires when either probe exceeds this. 7 °C (45 °F) is a common fresh-produce limit; frozen loads typically use −15 °C (5 °F). Values outside the range are silently ignored and the current value is kept. |
- | `temp_min_c` | `-25.0` | −60.0 to 20.0 °C | Cold / freeze protection threshold. An alert with `alert:"temp_cold"` fires when either probe drops below this. Useful for loads with a minimum-temperature requirement (e.g., live plants, some vaccines). Values outside the range, or a value ≥ `temp_max_c`, are silently ignored. |
- | `door_alert_sec` | `600` | 30–86400 seconds | Seconds the door can remain open before a `door_open_long` reminder fires. Default is 10 minutes — typical for a legitimate delivery stop. |
- | `sample_interval_sec` | `60` | 10–3600 seconds | Seconds between sensor samples and host sleep cycles. |
- | `summary_interval_min` | `60` | 1–1440 minutes | Minutes between `trailer_summary_cell.qo` Notes. Changing this also re-applies `hub.set outbound` so the Notecard's sync cadence tracks the new value. |
- | `alert_cooldown_sec` | `1800` | 60–86400 seconds | Global minimum seconds between any two temperature alerts, regardless of probe or excursion type. The firmware maintains a single `last_temp_alert_epoch` shared by all probes and alert types — a warm-excursion alert from probe 1 resets the same timer that would suppress a subsequent cold-excursion from probe 2. Prevents alert storms during slow-drift excursions; 30 minutes is a reasonable floor for operational response time. |
-
-6. **Configure routes.** Add [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for the following Notefiles:
- - `trailer_alert.qo` — alert event (door state change, temperature excursion). Delivered over the first available transport; the Notecard automatically selects cellular when in range and NTN satellite as a fallback. Route to your TMS, dispatch system, or on-call channel for real-time delivery. Transport information (cellular vs. satellite) is available in Notehub session metadata for each event.
- - `trailer_log_cell.qo` — per-sample record written every `sample_interval_sec` (default 60 seconds): both probe temperatures and door state. Cellular/WiFi only; never transmitted over NTN. Route to a cold-chain compliance archive or long-term data store. Gaps in this file during NTN-only coverage are expected. See §10.
- - `trailer_summary_cell.qo` — hourly compliance summary (mean/min/max per probe, door event count, current door state). Cellular/WiFi only. Route to the same cold-chain compliance destination as `trailer_log_cell.qo`.
-
- Separating alerts from log and summary data means routes can be configured at different urgency levels without filter logic on either end.
-
-### What you should see in Notehub
-
-- **`_session.qo`** — Notecard housekeeping; one per cellular or satellite session. The presence of these events confirms the radio is reaching Notehub.
-- **`trailer_log_cell.qo`** — one Note queued per `sample_interval_sec` (default every 60 seconds), cellular/WiFi only. Notes arrive in Notehub in batches on each outbound session (default 60 minutes, expect up to 60 Notes per delivery). Sample body:
- ```json
- { "t1_c": -1.4, "t2_c": -0.8, "door_open": false }
- ```
- Temperature fields showing `−127` mean the corresponding probe was not responding at the time of that sample. Gaps in this file during extended NTN-only coverage are expected; per-sample records resume once terrestrial connectivity returns.
-- **`trailer_summary_cell.qo`** — one per `summary_interval_min` (default hourly), cellular/WiFi only. Gaps in this file during extended NTN-only coverage are expected; summary Notes queued while NTN is the active transport are discarded at sync time (same `delete:true` mechanism as `trailer_log_cell.qo`) and are not retained for later terrestrial upload. Sample body:
- ```json
- {
- "t1_c": -1.4,
- "t2_c": -0.8,
- "t1_min_c": -2.1,
- "t1_max_c": -0.9,
- "t2_min_c": -1.6,
- "t2_max_c": -0.3,
- "door_events": 2,
- "door_open": false
- }
- ```
- A temperature field showing `−127` means the probe was not responding. In a summary Note this means no valid samples were recorded across the entire window; the same `−127` sentinel is used consistently in alert Notes when a probe fails to respond at the moment the alert fires. Treat `−127` as a sensor-fault flag in both Note types, never as a temperature near absolute zero.
-- **`trailer_alert.qo`** — generated on the next sample cycle after a threshold trip or door event occurs; transmitted immediately via `hub.sync` so the Notecard does not wait for the next outbound window. Delivered over the first available transport — cellular when in range, NTN satellite as fallback. Sample bodies:
- ```json
- { "alert": "temp_excursion", "t1_c": 9.2, "t2_c": 8.7, "door_open": true, "door_open_sec": 0, "lat": 41.8781, "lon": -87.6298 }
- { "alert": "door_open", "t1_c": -1.4, "t2_c": -0.8, "door_open": true, "door_open_sec": 0, "lat": 41.8781, "lon": -87.6298 }
- { "alert": "door_close", "t1_c": -1.4, "t2_c": -0.8, "door_open": false, "door_open_sec": 387, "lat": 41.8781, "lon": -87.6298 }
- ```
- `lat` and `lon` carry the last known GNSS fix at the moment the alert fired. Both fields are `0.0` when no fix has been acquired yet (e.g., first power-on with the GNSS antenna not yet having a clear sky view); treat `lat == 0.0 && lon == 0.0` as a no-fix sentinel in downstream analytics.
-
-
-## 7. Firmware Design
-
-The firmware spans three files: [`reefer_cold_chain_monitor.ino`](firmware/reefer_cold_chain_monitor/reefer_cold_chain_monitor.ino) contains `setup()`, `loop()`, and the top-level sample-cycle driver; [`reefer_cold_chain_monitor_helpers.h`](firmware/reefer_cold_chain_monitor/reefer_cold_chain_monitor_helpers.h) defines constants, `AppState`, clamp helpers, and function prototypes; and [`reefer_cold_chain_monitor_helpers.cpp`](firmware/reefer_cold_chain_monitor/reefer_cold_chain_monitor_helpers.cpp) implements sensor drivers, the door and temperature state machines, and all Notecard communication.
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Add the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs**, then install via Boards Manager (search "STM32 MCU based boards"). Select the **Cygnet** board (search for "Cygnet" in the board selector; it appears under the Blues board family in the current stm32duino core).
-- **`Blues Wireless Notecard`** library — [`note-arduino`](https://github.com/blues/note-arduino). Install via Arduino Library Manager: `arduino-cli lib install "Blues Wireless Notecard"`, or search for `Blues Wireless Notecard` in the IDE Library Manager and install the latest stable version. Check the [note-arduino releases](https://github.com/blues/note-arduino/releases) page for the current stable release.
-- **`OneWire`** — [`PaulStoffregen/OneWire`](https://github.com/PaulStoffregen/OneWire). Install via Library Manager.
-- **`DallasTemperature`** — [`milesburton/Arduino-Temperature-Control-Library`](https://github.com/milesburton/Arduino-Temperature-Control-Library). Install via Library Manager.
-
-**Flashing — Arduino IDE:** open `reefer_cold_chain_monitor.ino`, select the **Cygnet** board (search for "Cygnet" in the Boards Manager selector, it appears under the Blues board family in the current stm32duino core), and hit **Upload**.
-
-**Flashing — `arduino-cli`:**
-```bash
-# Find the exact FQBN for your installed core version (the name varies by stm32duino release)
-arduino-cli board listall | grep -i cygnet
-# Expected output on current stm32duino core:
-# Cygnet STMicroelectronics:stm32:Blues:pnum=CYGNET
-
-# Compile and upload — substitute the FQBN reported by the command above if it differs
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- firmware/reefer_cold_chain_monitor/
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/cu.usbmodem* firmware/reefer_cold_chain_monitor/
-```
-
-The exact FQBN is whatever the current `stm32duino` core ships for the Cygnet variant — the `board listall` command above is the authoritative source for your specific core version. Replace `/dev/cu.usbmodem*` with your actual port (`COMx` on Windows, `/dev/ttyACM*` on Linux). Open the serial monitor at **115200 baud** to watch `[boot]`, `[sensor]`, `[door]`, `[temp]`, `[summary]`, and `[alert]` log lines.
-
-### 7.2 Template format codes (compact encoding)
-
-The alert Notefile uses [compact format](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) — binary-encoded fields with fixed width to minimize satellite data consumption. Field types are named in the `note.template` request: `TFLOAT32` (4-byte IEEE-754), `TINT16` (2-byte signed), `TUINT32` (4-byte unsigned), `TBOOL` (boolean). These appear in the code as numeric constants: `14.1` = `TFLOAT32`, `12` = `TINT16`, `24` = `TUINT32`. The alert Notefile's compact+port encoding omits the standard JSON envelope and delivers only the declared body fields as binary records — critical for the 10 KB satellite data budget. The same format is fully supported on cellular; no special firmware handling needed.
-
-### 7.3 Modules
-
-| Responsibility | Function |
-|---|---|
-| First-boot Notecard config (`hub.set`, `card.transport`, `card.location.mode`) | `hubConfigure` |
-| Re-sync `hub.set` outbound when summary interval changes | `applyHubSetIfChanged` |
-| Compact Note template definition | `defineTemplates` |
-| Environment-variable refresh (each wake) | `fetchEnvOverrides` |
-| DS18B20 temperature reading (1-Wire) | `readTemperatures` |
-| Door reed switch reading | `readDoorState` |
-| Door open/close/long-open state machine | `checkDoorEvents` |
-| Temperature excursion detection + dedup | `checkTemperatureExcursion` |
-| Rolling window accumulation | `accumulateSummary` |
-| Hourly summary Note | `sendSummary` |
-| Immediate-sync alert Note | `sendAlert` |
-| Epoch time from Notecard RTC | `getEpochTime` |
-| State persist/restore | `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-
-### 7.4 Sensor reading strategy
-
-**DS18B20 probes.** Both sensors share the single 1-Wire bus on A0. `probes.requestTemperatures()` issues a simultaneous conversion to both sensors and blocks ~750 milliseconds at 12-bit resolution (0.0625 °C steps). `getTempCByIndex(0)` and `getTempCByIndex(1)` return the two readings. The DallasTemperature library returns `DEVICE_DISCONNECTED_C` (−127 °C) for any probe that fails to respond; the firmware treats readings below `TEMP_INVALID + 1.0` as invalid and excludes them from summary averages without affecting the other probe's accumulation.
-
-**Door reed switch.** A simple `digitalRead(DOOR_PIN)` per sample. The Adafruit #375 is a normally-open switch; with `INPUT_PULLUP` and the switch between `A1` and `GND`, a closed door reads LOW and an open door reads HIGH. Instantaneous reads are appropriate here because a real trailer door remains in each state for seconds to minutes.
-
-### 7.5 Event payload design
-
-Three [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) Notefiles hold the data produced by the firmware. The alert Notefile (`trailer_alert.qo`) uses compact format with a port assignment. Compact encoding omits the standard Note metadata envelope and stores only declared body fields as fixed-length binary records, which is critical for the 10 KB satellite data budget, and a port number is required for NTN (satellite) mode. Compact+port is also fully supported on cellular, so the same Notefile is delivered correctly over either transport. The cellular/WiFi-only Notefiles (`trailer_log_cell.qo` and `trailer_summary_cell.qo`) are template-backed for consistent schema enforcement but use standard JSON encoding without a port, and carry `delete:true`. Per the [Blues Satellite Best Practices](https://dev.blues.io/starnote/satellite-best-practices/) documentation, a cellular/WiFi-only Notefile template (no `format` or `port`) with `delete:true` causes the Notecard to discard its queued Notes at sync time when NTN is the active transport, so these Notefiles are never transmitted over satellite and do not consume the bundled satellite data budget. (The `_cell` suffix in these Notefile names denotes terrestrial transport; they are never transmitted over NTN.)
-
-**`trailer_log_cell.qo`: per-sample, queued, cellular/WiFi only:**
-```json
-{ "t1_c": -1.4, "t2_c": -0.8, "door_open": false }
-```
-
-**`trailer_summary_cell.qo`: hourly, queued, cellular/WiFi only:**
-```json
-{
- "t1_c": -1.4,
- "t2_c": -0.8,
- "t1_min_c": -2.1, "t1_max_c": -0.9,
- "t2_min_c": -1.6, "t2_max_c": -0.3,
- "door_events": 2,
- "door_open": false
-}
-```
-
-**`trailer_alert.qo`: immediate, enqueued then `hub.sync`, cellular or NTN transport:**
-```json
-{
- "alert": "temp_excursion",
- "t1_c": 9.2, "t2_c": 8.7,
- "door_open": true,
- "door_open_sec": 0,
- "lat": 41.8781, "lon": -87.6298
-}
-```
-
-On every alert the firmware enqueues the alert Notefile (without `sync:true`) and then issues a `hub.sync` request to wake the radio. Decoupling the sync trigger from the enqueue avoids a race where `sync:true` on the `note.add` could trigger a sync before the Note is fully committed. The Notecard selects cellular when in range and falls back to NTN satellite otherwise; no radio-selection logic is required in the firmware.
-
-Alert type values: `door_open` (door just opened), `door_close` (door just closed; `door_open_sec` carries the open duration), `door_open_long` (door still open past `door_alert_sec`), `temp_excursion` (probe above `temp_max_c`), `temp_cold` (probe below `temp_min_c`).
-
-The `door_open_sec` field is `TUINT32` (4-byte unsigned integer) in the template, supporting door-open durations up to ~136 years. A probe that fails to respond appears as `−127` in the temperature fields of any Note type (`trailer_log_cell.qo`, `trailer_summary_cell.qo`, and `trailer_alert.qo` alike) and should be treated as a sensor-fault flag, not a real temperature. Door-event alerts (`door_open`, `door_close`, `door_open_long`) include the live probe readings sampled at the moment the alert fires, so `−127` in a door alert means the probe was genuinely unresponsive at that instant, not that temperatures were intentionally omitted. `lat` and `lon` are `TFLOAT32` fields carrying the last GNSS fix; both are `0.0` when no fix is available — treat that pair as a no-fix sentinel.
-
-### 7.6 Power and sync strategy
-
-Even though the trailer has a 12 V supply, keeping the host MCU asleep between samples dramatically reduces heat in the enclosure and makes the firmware structure port cleanly to a battery-backed variant (Scoop or otherwise). After each sample cycle the host calls `NotePayloadSaveAndSleep`, a `note-arduino` helper that serializes the in-RAM `AppState` struct into Notecard flash and issues a [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) sleep request to cut host power for `sample_interval_sec` seconds.
-
-The Notecard itself idles at ~8–18 µA between sessions. Per-sample log Notes and hourly summary Notes accumulate in the Notecard's on-device queue and flush in a single outbound session. Door and temperature alerts are enqueued and then trigger a `hub.sync` request, which bypasses the outbound timer and wakes the radio immediately using whatever network is available: cellular if in range, NTN satellite otherwise.
-
-Sampling and transmission are deliberately decoupled: sensors sample every 60 seconds, but the radio connects only once per hour (plus on-demand for alerts). The hourly session also pulls fresh environment variables on the inbound cadence (default 120 minutes). When cellular is unavailable and the Notecard uses NTN for a periodic session, `trailer_log_cell.qo` and `trailer_summary_cell.qo` Notes are discarded at sync time — their cellular/WiFi-only `delete:true` templates cause the Notecard to clear those queues rather than transmit over satellite, so only pending alert Notes carry payload data over the satellite link. Session establishment overhead (Skylo protocol + housekeeping) still occurs on every NTN session regardless of alert payload.
-
-### 7.7 Retry and error handling
-
-- The first Notecard I²C transaction (in `hubConfigure`) uses `sendRequestWithRetry(req, 10)` to absorb the cold-boot race condition documented in the note-arduino library.
-- `readTemperatures` excludes any probe returning `DEVICE_DISCONNECTED_C` from all accumulation. If both probes fail for the entire summary window, all six temperature fields in the summary carry the `−127` sentinel rather than a misleading mean-of-zero.
-- `fetchEnvOverrides` silently skips missing variables and retains the current firmware default, so a Notehub project with no environment variables set is always valid.
-- Alert deduplication: temperature alerts are rate-limited by `alert_cooldown_sec` (default 30 minutes) so a slow-drifting probe doesn't flood the downstream on-call channel.
-- Door open-long reminder fires once per open event (`door_long_alert_sent` flag in persisted state). A door that stays open across multiple sleep cycles produces exactly one reminder, not one per sample.
-
-### 7.8 Key code snippet 1: compact template definition
-
-Templates registered at first boot make each Note a fixed-length binary record optimized for satellite transport. The `"compact"` format and `port` assignment on the alert Notefile satisfy NTN requirements and are also fully supported on cellular — the same file is delivered correctly over either transport. The cellular/WiFi-only Notefiles do not use compact format or a port, and carry `delete:true`: the Notecard discards queued Notes from cellular/WiFi-only Notefiles when NTN is the active transport at sync time (see [Blues Satellite Best Practices](https://dev.blues.io/starnote/satellite-best-practices/)).
-
-```cpp
-// Alert template (trailer_alert.qo) — compact + port, no delete:true
-// Delivered over the first available transport (cellular or NTN satellite).
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "trailer_alert.qo");
-JAddNumberToObject(req, "port", 51);
-JAddStringToObject(req, "format", "compact");
-// No delete:true — alert notes are never purged before a sync.
-J *body = JAddObjectToObject(req, "body");
-// Exemplar string sets the field width; use the longest alert value (14 chars).
-JAddStringToObject(body, "alert", "temp_excursion");
-JAddNumberToObject(body, "t1_c", TFLOAT32);
-JAddNumberToObject(body, "t2_c", TFLOAT32);
-JAddBoolToObject(body, "door_open", TBOOL);
-JAddNumberToObject(body, "door_open_sec", TUINT32);
-JAddNumberToObject(body, "lat", TFLOAT32);
-JAddNumberToObject(body, "lon", TFLOAT32);
-notecard.sendRequest(req);
-
-// Per-sample log template (trailer_log_cell.qo) — cellular/WiFi only
-req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "trailer_log_cell.qo");
-JAddBoolToObject(req, "delete", true); // non-NTN: discarded when NTN is active transport
-body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "t1_c", TFLOAT32);
-JAddNumberToObject(body, "t2_c", TFLOAT32);
-JAddBoolToObject(body, "door_open", TBOOL);
-notecard.sendRequest(req);
-
-// Hourly summary template (trailer_summary_cell.qo) — cellular/WiFi only
-req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", "trailer_summary_cell.qo");
-JAddBoolToObject(req, "delete", true); // non-NTN: discarded when NTN is active transport
-body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "t1_c", TFLOAT32);
-JAddNumberToObject(body, "t2_c", TFLOAT32);
-JAddNumberToObject(body, "t1_min_c", TFLOAT32);
-JAddNumberToObject(body, "t1_max_c", TFLOAT32);
-JAddNumberToObject(body, "t2_min_c", TFLOAT32);
-JAddNumberToObject(body, "t2_max_c", TFLOAT32);
-JAddNumberToObject(body, "door_events", TINT16);
-JAddBoolToObject(body, "door_open", TBOOL);
-notecard.sendRequest(req);
-```
-
-### 7.9 Key code snippet 2: immediate-sync alert
-
-The alert Notefile is enqueued without `sync:true`; after the enqueue succeeds a `hub.sync` call wakes the radio immediately. On the NOTE-NBGLWX the Notecard selects cellular if in coverage and falls back to the Skylo satellite radio otherwise — the firmware never needs to know which. `getLocation()` reads the last GNSS fix so every alert is geo-stamped at the moment it fires.
-
-`hub.sync` is treated as part of the critical alert path: `sendAlert()` checks the response for `NULL` and `responseError()`, and only returns `true` after the sync itself succeeds. If it fails, the pending-alert latch in `AppState` stays set and the next wake retries the sync (skipping the already-queued `note.add` call via the `done` flag).
-
-```cpp
-float lat = 0.0f, lon = 0.0f;
-getLocation(lat, lon); // last known fix; (0.0, 0.0) = no fix yet
-
-// Step 1 — enqueue alert (no sync:true); compact+port works on cellular and NTN
-J *req = notecard.newRequest("note.add");
-JAddStringToObject(req, "file", "trailer_alert.qo");
-J *body = JAddObjectToObject(req, "body");
-JAddStringToObject(body, "alert", "temp_excursion");
-JAddNumberToObject(body, "t1_c", (double)t1);
-JAddNumberToObject(body, "t2_c", (double)t2);
-JAddBoolToObject(body, "door_open", s.door_open);
-JAddNumberToObject(body, "door_open_sec", 0.0);
-JAddNumberToObject(body, "lat", (double)lat);
-JAddNumberToObject(body, "lon", (double)lon);
-J *rsp = notecard.requestAndResponse(req);
-if (rsp != NULL) {
- if (!notecard.responseError(rsp)) { done = true; }
- notecard.deleteResponse(rsp);
-}
-
-// Step 2 — trigger immediate sync once the note is enqueued
-if (done) {
- req = notecard.newRequest("hub.sync");
- rsp = notecard.requestAndResponse(req);
- bool sync_ok = (rsp != NULL && !notecard.responseError(rsp));
- if (rsp != NULL) notecard.deleteResponse(rsp);
- // Returns true only when sync_ok: keeps pending-alert latch on failure
-}
-```
-
-### 7.10 Key code snippet 3: state persistence and sleep
-
-`NotePayloadSaveAndSleep` serializes `AppState` into Notecard flash and then issues `card.attn` to cut host power. On the next wake, `setup()` runs from cold and `NotePayloadRetrieveAfterSleep` restores the struct — door timestamps, accumulator sums, alert epochs, and all.
-
-```cpp
-NotePayloadDesc out = {};
-NotePayloadAddSegment(&out, STATE_SEG_ID, &s, sizeof(s));
-NotePayloadSaveAndSleep(&out, (int)g_sampleIntervalSec, NULL);
-```
-
-
-## 8. Data Flow
-
-
-
-Every 60 seconds the firmware wakes, reads both temperature probes and the door pin, runs two independent checks (door state machine and temperature excursion), logs the raw sample, accumulates data into the rolling hourly window, and sleeps again. The two checks are independent: a door event and a simultaneous temperature excursion each produce their own alert.
-
-**Measured each sample cycle (every 60 seconds):** forward-air temperature °C (probe 1), return-air temperature °C (probe 2), door state (open/closed). Every sample is individually logged to `trailer_log_cell.qo` (cellular/WiFi only; discarded when the Notecard connects via NTN). Readings also accumulate into a rolling hourly summary window (mean, min, max per probe). **GNSS position** is maintained autonomously by the Notecard in the background (`card.location.mode` `periodic`, 600-second interval, motion-gated); the host MCU does not query location on each sample cycle and instead reads the last known fix only when emitting an alert, so every alert is geo-stamped at event detection time without a per-sample GNSS query.
-
-**Transmitted:**
-- `trailer_alert.qo` — evaluated and queued during the next sample cycle (up to `sample_interval_sec` after the event, default 60 seconds); transmitted immediately via `hub.sync` (bypasses the outbound timer) on: door opens, door closes, door open past `door_alert_sec`, probe exceeds `temp_max_c`, probe drops below `temp_min_c`. Delivered over the first available transport (cellular preferred; NTN satellite fallback). Temperature alerts are rate-limited by `alert_cooldown_sec`.
-- `trailer_log_cell.qo` — one Note per sample cycle (default every 60 seconds), cellular/WiFi only, queued and shipped on the next outbound sync. Gaps expected during NTN-only coverage (see §10).
-- `trailer_summary_cell.qo` — one Note per `summary_interval_min` (default 60 minutes), cellular/WiFi only, queued and shipped on the next outbound sync. Contains mean, min, and max for each probe plus door-open count and current door state.
-
-**Location.** `hubConfigure` calls [`card.location.mode`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location-mode) with `mode:"periodic"` and `seconds:600`. The Notecard maintains location autonomously in the background — `periodic` mode samples at the configured interval only when the Notecard detects motion, so the GNSS radio activates roughly every 10 minutes while the trailer is moving and stays off while it is stationary. The host MCU does not poll `card.location` on each 60-second sample cycle; instead, `sendAlert` issues a single `card.location` request immediately before writing each alert Note, embedding the most recent fix as `lat`/`lon` fields. This means every alert is geo-stamped with the trailer's position at event-detection time without adding a GNSS query to the routine sample cycle. When no fix has been acquired yet (first power-on, GNSS antenna obstructed), both fields are `0.0`. The GNSS antenna must have an unobstructed sky view — mount it on the trailer roof or exterior wall, away from the refrigeration unit, per [§5](#5-wiring-and-assembly).
-
-**Routing.** All three Notefiles land in Notehub. From there, routes fan them out to downstream systems. Typical pattern: `trailer_alert.qo` → TMS or messaging gateway (real-time dispatch); `trailer_log_cell.qo` and `trailer_summary_cell.qo` → cold-chain compliance database.
-
-**Alert triggers** (appear in `trailer_alert.qo`; delivered over cellular or NTN satellite):
-- `door_open` — door-closed-to-open transition detected. Immediate sync.
-- `door_close` — door-open-to-closed transition; `door_open_sec` carries the open duration.
-- `door_open_long` — door still open after `door_alert_sec` seconds (default 10 minutes). Fires once per open event.
-- `temp_excursion` — either probe above `temp_max_c`. Subject to `alert_cooldown_sec` dedup.
-- `temp_cold` — either probe below `temp_min_c`. Subject to `alert_cooldown_sec` dedup.
-
-
-## 9. Validation and Testing
-
-**Expected steady-state cadence.** On a healthy, closed, in-range trailer: `trailer_log_cell.qo` Notes every 60 seconds (queued locally; arriving in Notehub on the hourly outbound session, up to 60 per batch), one `trailer_summary_cell.qo` per hour, zero alert events. The first batch of log Notes arrives in Notehub at the first outbound session (default 60 minutes); alerts appear within one sample cycle (`sample_interval_sec`, default 60 seconds) of the event plus network-establishment time in `trailer_alert.qo`.
-
-**Functional validation.** To confirm alert flow without waiting for a real excursion:
-- **Temperature (warm-excursion path):** on a room-temperature bench the probes already read well above the 7 °C default, so the simplest reproducible test is to lower `temp_max_c` to 2–3 °C below the current probe reading (check the `[sensor]` lines in the serial output for the current value, then set the Fleet environment variable accordingly). The next sample cycle fires the `temp_excursion` alert without touching the probes. Restore `temp_max_c` to the operational value when done.
-- **Temperature (cold-excursion path):** submerge one probe in an ice-water bath (~0 °C) and temporarily raise `temp_min_c` to `5.0` in the Fleet environment variables. The next inbound sync pulls the new value, and the next sample with the iced probe triggers a `temp_cold` alert. Restore `temp_min_c` to `-25.0` (or your operational value) after confirming the alert.
-- **Door:** separate the door sensor magnet from the housing. A `door_open` alert should appear within the next sample cycle. Reconnect the magnet and verify `door_close` arrives with a non-zero `door_open_sec`.
-- **Threshold tuning:** lower `temp_max_c` to `0.0` in the Fleet's environment variables; the next inbound sync pulls the new value, and the next sample cycle with any probe above 0 °C fires the alert.
-- **Location:** confirm the GNSS antenna has a clear sky view outdoors. After at least one full `card.location.mode` fix interval (≤ 10 minutes on a moving or recently-moved unit), trigger a door or temperature alert and verify `lat` and `lon` are non-zero in the resulting `trailer_alert.qo` Note body in Notehub. A `(0.0, 0.0)` pair means no fix has been acquired yet — allow more time or confirm the GPS antenna cable and connector are seated.
-
-**Power validation with Mojo.** Notecard for Skylo's published figures: ~8–18 µA idle (radio off between syncs); ~250 mA average with peaks approaching 2 A during cellular transmit; satellite sessions are similar in current envelope but longer in duration.
-
-Place the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the 5 V supply and the Notecarrier CX `+VBAT` pad. The expected trace in steady state:
-
-| Phase | Expected current |
-|---|---|
-| Host + Notecard sleeping between samples | ~20–30 µA |
-| Host awake (sensor reads + Notecard I²C) | ~5–15 mA for ~1–2 seconds per sample |
-| Notecard cellular session (hourly) | ~250 mA average, 15–45 seconds |
-| Notecard satellite session (NTN) | ~200–400 mA average, typically longer than LTE-M |
-
-A useful bench target with defaults (60 seconds sample, 60 minutes summary, no alerts): roughly **30–60 mAh per 24 h** on cellular, dominated by the once-per-hour radio burst. Satellite-only operation will consume more per session due to longer link-establishment time.
-
-> **Measurement scope.** Mojo sits on the 5 V output side of the Pololu regulator and measures only the downstream Notecarrier + Notecard draw at 5 V. Actual consumption from the 12 V trailer battery will be higher: the Pololu adds its own quiescent current and conversion losses (the D24V22F5 is rated 85–95 % efficient under load, and draws quiescent current even at no load). For installations where parked-trailer battery drain is a concern, complement Mojo's 5 V measurement with an upstream current measurement on the 12 V feed, or consider a step-down regulator with lower quiescent current if the unit will sit parked for extended periods.
-
-Two anomaly patterns stand out immediately on a Mojo trace:
-- **Host never sleeping:** flat 10–30 mA continuous baseline — almost always a `card.attn` power-gating regression (verify you're on a Notecarrier CX and that the ATTN signal path is intact).
-- **Excessive hourly bursts:** each session runs 2–3 minutes instead of 15–45 seconds, usually weak signal or frequent satellite fallback (check antenna routing and site coverage).
-
-Mojo is a **bench and commissioning tool**; production units don't require it.
-
-
-A Notecarrier CX and a Notecard for Skylo pair with two DS18B20 probes and a magnetic reed switch to watch the two failure modes that drive most reefer trailer cargo loss: temperature excursion and unauthorized door access. The firmware samples every minute, queues each raw sample locally for Notehub delivery on the next outbound session (`trailer_log_cell.qo`), fires an alert on the next sample cycle (up to ~60 seconds) when any threshold trips via whatever radio is available (`trailer_alert.qo`, delivered over cellular or NTN satellite automatically), and ships a compact hourly summary (`trailer_summary_cell.qo`) for compliance and trending. The cellular + satellite multi-RAT design means the monitor stays connected as the trailer moves from an urban distribution center through rural interstates to a port staging yard, without any network configuration, SIM swapping, or carrier negotiation required.
-
-The same firmware structure is a natural starting point for pharmaceutical cold chain, cross-border intermodal containers, or any mobile insulated enclosure where a temperature log and a door audit trail have regulatory or financial weight.
-
-
-## 10. Troubleshooting
-
-| Symptom | Likely Cause | What to Check |
-|---------|--------------|---------------|
-| **Device never appears in Notehub** | `PRODUCT_UID` is still the empty default, or the unit has not yet established a session on any radio. | Confirm `PRODUCT_UID` in [`reefer_cold_chain_monitor_helpers.h`](firmware/reefer_cold_chain_monitor/reefer_cold_chain_monitor_helpers.h) matches the value shown in your Notehub project. Watch the serial monitor at 115200 baud for `[boot]` and `_session.qo` lines; a session typically completes within a few minutes of first power-on if any radio is in range. If only NTN coverage is available, expect a longer first-session time and confirm the GNSS and `MAIN` antennas have an unobstructed sky view. |
-| **One or both DS18B20 probes report `-127`** | Probe wiring fault, missing 4.7 kOhm pull-up on the 1-Wire bus, or a damaged probe lead. | Verify VDD goes to `+3V3`, GND to `GND`, and data to `A0` on every probe. Confirm the 4.7 kOhm pull-up (included in the Adafruit #381 bag) sits between `+3V3` and `A0`. Power-cycle with one probe at a time to isolate which lead is failing. The firmware treats `-127` as a sensor-fault sentinel, so a single failed probe does not corrupt the other probe's summary average. |
-| **Implausible temperature readings (e.g. +85 then drifting)** | DS18B20 power-on default is `+85` until the first conversion completes; long unshielded cable runs can also pick up noise. | Allow at least one full sample cycle before trusting a probe reading. Keep probe leads away from reefer wiring harnesses and inverter motors. If the issue persists, shorten the cable run or swap to a shielded extension. |
-| **Door state stuck `false` (closed) when the door is open** | Magnet still in proximity to the reed switch, or the housing and magnet are mounted in reversed positions. | Remember the Adafruit #375 is normally-open: closed door (magnet near switch) reads LOW, open door reads HIGH. Mount the housing on the stationary frame and the magnet on the moving leaf, not the other way around. Verify the gap closes to within 15 mm when the door shuts. |
-| **Door state stuck `true` (open)** | Cut or pinched 22 AWG extension cable, oxidized splice, or a failed reed switch. | Continuity-test the extension wire end to end with the magnet held against the housing. Re-seat any butt splices and reseal them with a gel-filled connector if water has reached the joint. |
-| **Cellular sessions succeed but no NTN sessions ever fire** | Default selection is satisfied by terrestrial coverage, so NTN never gets exercised. | This is expected behavior on the `wifi-cell-ntn` policy: NTN is a fallback, not a parallel path. To confirm NTN is healthy, take the unit to a known cellular dead zone or temporarily disable the cellular and WiFi paths via [`card.transport`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-transport) on the Notecard, then watch for `_session.qo` entries that report a `ntn` transport in their session metadata. |
-| **Temperature alerts firing on a healthy load** | Threshold values do not match the actual cargo class, or one probe is sitting against a cold or warm spot inside the trailer. | Open the Fleet's environment variables and confirm `temp_max_c` and `temp_min_c` match the cargo (fresh produce, frozen, or pharmaceutical). Compare both probe values in `trailer_log_cell.qo`; if one consistently disagrees with the other by several degrees, relocate it away from the evaporator outlet, the door seal, or direct return-air flow. |
-| **Hourly summaries arrive but the next outbound session is hours late** | The trailer is parked in NTN-only coverage, so `trailer_log_cell.qo` and `trailer_summary_cell.qo` Notes are discarded at sync time and only alerts ride the satellite link. | This is the documented satellite-budget protection behavior. Once terrestrial coverage returns, per-sample logging and summaries resume on the next outbound session; the hourly cadence can be raised by lowering `summary_interval_min` in environment variables. |
-| **Battery / power loss after long parked dwell** | Pololu D24V22F5 quiescent draw on the 12 V trailer battery, or the regulator is wired ahead of an always-on circuit instead of an accessory feed. | Tap the 12 V supply from a switched accessory circuit so the unit drops out when the trailer is fully parked. For installations that must run continuously, plan for the regulator's own quiescent current on top of the Notecard's idle draw, and consider a lower-quiescent step-down regulator (see the measurement-scope Note in [§9](#9-validation-and-testing)). |
-| **GNSS fields stuck at `0.0, 0.0`** | GNSS antenna does not have a clear sky view, the u.FL connector is not seated, or the trailer has not moved long enough to satisfy `card.location.mode`'s motion-gated periodic sample. | Reseat both u.FL connectors on the Notecard and reroute the GNSS antenna to the trailer roof or exterior wall, away from the refrigeration unit. Drive the trailer for at least one full 600 second interval and confirm the next alert carries a non-zero fix. |
-| **Antenna or sky-view problems on Skylo** | Antenna substituted with a non-Skylo-certified part, or `MAIN` antenna mounted inside a metal enclosure. | Confirm the included Skylo-certified flexible antenna is on the `MAIN` port; substituting a different cellular or NTN whip decertifies the device on Skylo's network. Route the antenna outside the NEMA 4X enclosure and any steel trailer structure via a sealed IP68 cable gland. See [§5](#5-wiring-and-assembly) and the [Blues Antenna Guide](https://dev.blues.io/datasheets/application-notes/antenna-guide/). |
-
-For anything not covered above, search or post on the [Blues community forum](https://discuss.blues.com).
-
-
-## 11. Limitations and Next Steps
-
-The design hits the two failure modes that account for most refrigerated cargo loss (excursion and unauthorized door access) using direct sensor input and no OEM reefer integration, so it works across heterogeneous fleets. Humidity, multi-zone mapping, J1939 telemetry, and geofence logic are deliberate scope choices to be added where a deployment warrants them.
-
-### Simplified for the POC
-
-The simplifications below are deliberate scope choices. Each marks a place where a real fleet deployment will want to add another sensor, finer placement guidance, or an OEM integration once the basic excursion-and-door monitor is proven in the field.
-
-**Two probes only.** A 53 foot trailer can stratify several degrees from front to rear and from floor to ceiling. This design fixes one DS18B20 near the forward evaporator and one near the rear return-air panel, which is enough to catch reefer failure and gross excursions but is **not a full thermal map of the cargo zone**.
-
-**No humidity or dew-point sensor.** Some loads — leafy greens, certain pharmaceuticals, paper goods — are humidity-sensitive in addition to temperature-sensitive, but the current Notes carry temperature only.
-
-**Coarse probe placement guidance.** Section 5 specifies forward and rear positions, but does not prescribe an evaporator-vs-cargo-zone placement scheme calibrated to a particular load type or trailer geometry. Operators are expected to refine probe placement during their first commissioning runs.
-
-**Reed switch is binary, not multi-state.** The Adafruit #375 reports open or closed only. It cannot distinguish a door cracked an inch from a door swung wide, and it has no concept of which door (left or right) was actuated on a split-rear configuration.
-
-**No driver or dispatcher app integration.** Alerts are routed from Notehub to whatever downstream destination is configured; this project does not ship a mobile app for the driver or a dashboard for dispatch.
-
-**No reefer ECU integration.** The design is intentionally OEM-agnostic, so it cannot read setpoint, defrost cycle, refrigerant pressure, fuel level, or alarm codes from the reefer controller itself. Operators see cargo-zone temperature, not reefer health.
-
-**Single 12 V feed assumption.** The wiring assumes a standard trailer accessory circuit. Battery-only installations or installations that need to survive a fully parked trailer for days would benefit from a different power architecture, including an upstream regulator with lower quiescent draw.
-
-### Production Next Steps
-
-Once the basic monitor is running across a real fleet, the following extensions are the natural progression, roughly from the most immediately useful to the most integration-dependent.
-
-**Add humidity (and dew point) sensing** by wiring an [SHT41](https://www.adafruit.com/product/5776) on the Qwiic bus and adding `rh_pct` and `dewpoint_c` fields to the per-sample, summary, and alert Notes. The compact alert template can absorb the additional fields without breaking the satellite data budget.
-
-**Multi-zone temperature mapping** moves from two probes to four or six DS18B20s on the same 1-Wire bus (the library and template can scale), emitting per-zone min, mean, and max in the hourly summary. This is helpful for high-value loads where a hot spot in one corner of the trailer is the early warning.
-
-**Reefer ECU integration** adds a J1939 or CAN tap for fleets with homogeneous reefer fleets, so setpoint, defrost, fuel level, and alarm codes ride along with the cargo-zone telemetry. Pair it with the existing Notes rather than replacing them.
-
-**Geofence-aware alerting** uses the `lat` and `lon` fields already on every alert to suppress door-open events at known authorized stops (yard, customer DC, fuel network) and elevate door events at unexpected locations. This logic is best run in Notehub or downstream rather than on-device.
-
-**Notecard Outboard DFU** wires [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) so threshold logic updates and bug fixes can ship over the air, including over NTN where appropriate.
-
-**Compliance reporting for FSMA-Cold and HACCP** layers a Notehub route into a compliance archive that aggregates per-sample logs and hourly summaries into per-shipment PDF reports, suitable for FSMA Sanitary Transportation rule audits and HACCP records for food shippers, or for pharma cold-chain release decisions.
-
-
-## 12. Summary
-
-The fleet dispatcher now has the two signals that matter most: a geo-stamped alert within roughly a minute when a door opens at an unexpected location, and a temperature excursion caught while the load can still be re-routed. Notecard for Skylo picks cellular, WiFi, or NTN satellite at every moment of a haul without any firmware logic, keeping the bundled satellite data budget intact by discarding the per-sample and hourly summary Notefiles at sync time when NTN is the active transport, while alerts ride the satellite link regardless. For shippers under FSMA Sanitary Transportation rules or HACCP records, and for pharmaceutical cold-chain operators on GDP terms, those same per-sample logs and hourly summaries become the independent condition record that audits and freight claims demand.
-
-The same skeleton carries to pharma cold chain, cross-border intermodal containers, frozen-food long-haul, or any mobile insulated enclosure where a temperature log and a door audit trail have regulatory or financial weight.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/reefer-trailer-cold-chain-door-event-monitor/)**
diff --git a/88-reefer-trailer-cold-chain-door-event-monitor/banner.png b/88-reefer-trailer-cold-chain-door-event-monitor/banner.png
deleted file mode 100644
index 72bb184d..00000000
Binary files a/88-reefer-trailer-cold-chain-door-event-monitor/banner.png and /dev/null differ
diff --git a/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/01-system-architecture.svg b/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/01-system-architecture.svg
deleted file mode 100644
index 8ab299c2..00000000
--- a/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-
diff --git a/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/02-wiring-assembly.svg b/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index b6eff459..00000000
--- a/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,110 +0,0 @@
-
diff --git a/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/03-data-flow.svg b/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/03-data-flow.svg
deleted file mode 100644
index f44cf4ac..00000000
--- a/88-reefer-trailer-cold-chain-door-event-monitor/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,100 +0,0 @@
-
diff --git a/89-pallet-attached-cold-chain-logger/README.md b/89-pallet-attached-cold-chain-logger/README.md
index 06582e75..df8fbc9b 100644
--- a/89-pallet-attached-cold-chain-logger/README.md
+++ b/89-pallet-attached-cold-chain-logger/README.md
@@ -1,552 +1,5 @@
# Pallet-Attached Cold Chain Logger
-
+A pallet-attached cold-chain logger for pharma and food shippers that provides an independent, shipper-controlled condition record and dispatches alerts when temperature, humidity, shock, tilt, or cargo-bay-opening thresholds are crossed.
-
-
-This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, [feel free to reach out](https://blues.com/landing-pages/accelerators-contact-us/?accelerator=Shipper-Owned%20Cargo-Level%20Cold%20Chain%20Monitor).
-
-**Building with an AI assistant?** Point it at this accelerator's firmware — [firmware/cargo_cold_chain_monitor/](firmware/cargo_cold_chain_monitor/) — so it can learn how the device works and help you adapt it to your hardware.
-
-
-
-This project is a pallet-attached cold chain logger, a [supply chain tracking](https://blues.com/solutions-supply-chain-tracking/) reference design, for pharma and food shippers who cannot afford to trust the reefer unit's built-in telematics. A handful of sensors, a [Blues Notecard for Skylo](https://shop.blues.com/products/notecard?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link), and a [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) give you an independent, shipper-controlled condition record that travels with the cargo — through loading docks, over-the-road transit, port staging, and customs DCs, and dispatches an alert Note when a temperature, humidity, shock, tilt, or cargo-bay-opening threshold is crossed.
-
-
-## 1. Project Overview
-
-**The problem.** When a pharma manufacturer ships a pallet of biologics or a food producer ships a truckload of fresh produce, the temperature data logged by the reefer unit's built-in telematics has a fundamental blind spot: those sensors read the air temperature near the refrigeration unit, not the air temperature where the cargo actually sits. A pallet in the back corner of a 53-foot trailer can run 5–8°F warmer than what the reefer controller reports, especially when the trailer has been standing in a distribution center (DC) yard waiting for a dock door, or when a forklift holds the door open for an extended loading sequence.
-
-In regulated industries like pharma and food distribution, "the reefer says it was fine" is not a sufficient basis for cargo acceptance or rejection decisions. Insurance adjusters, freight claims teams, and quality managers need continuous, independent, pallet-level condition data (data the shipper controls, not the carrier) with timestamps that can be matched to the shipment's **BOL** (bill of lading) and delivery receipt. When a shipment is suspect, the side with independent data makes faster, better-informed diversion and acceptance decisions.
-
-This project builds on the dedicated temperature logger concept: a pallet-attached logger that records a complete, tamper-evident condition history through the entire journey. Temperature is measured by a calibrated PT100 Class A RTD probe (±0.15 °C per IEC 60751), sourced with a NIST-traceable calibration certificate from the probe supplier. Every five-minute sample cycle generates a log entry with a monotonic sequence number, a rolling integrity chain hash, and a boot-segment counter (`boot_seg`) that increments on every device cold boot. Within each `boot_seg`, a downstream consumer can replay the chain from seq=1 (seed=0) to verify that no records were inserted, deleted, or modified in transit. On-device state tracking distinguishes warehouse dwell from active transit and cargo-bay-open handling events. It dynamically extends both summary interval and outbound sync cadence during long dwell periods to reduce satellite session frequency and NTN data cost, and emits an immediate state-change Note whenever the shipment transitions between states.
-
-**Why Notecard.** A single refrigerated shipment can cross three carriers, two countries, a port container terminal, and an ocean transit in the course of a week, each environment with different wireless coverage characteristics. Loading dock interiors are cellular dead zones. Ocean vessels transit thousands of miles with no terrestrial coverage. Customs DCs and bonded warehouses have inconsistent cellular coverage and almost never permit carriers' IoT devices onto their networks.
-
-
-
-> **Note on reefer standards:** This project does **not** implement J2497, J1939, or proprietary reefer protocols (Carrier DataLink, Thermo King DSR). It is a shipper-owned, independent cold-chain monitor that operates alongside or in parallel with carrier telematics, not as a reefer integration. The design is intentionally isolated: the logger measures cargo-bay conditions, not reefer controller state, and routes data to shipper systems (TMS, quality, analytics), not carrier networks.
-
-This project uses [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)), an all-in-one module that packs cellular (LTE-M, 2G/3G), WiFi (2.4 GHz), and satellite ([Skylo](https://www.skylo.tech/resources/geographical-coverage) NTN, non-terrestrial network) into a single M.2 form-factor Notecard. The device selects the best available radio automatically: WiFi in a connected DC where credentials have been provisioned on the Notecard (see [§6 WiFi provisioning](#6-notehub-setup)), cellular over road and rail, satellite where neither is available and the antenna has sky exposure. Queued Notes in Notecard flash carry their original timestamps and are transmitted intact when any radio comes back; coverage gaps produce store-and-forward gaps, not data loss. No SIM provisioning, no per-country certification cycle (Blues ships pre-certified globally), and no site network credentials to manage at every waypoint.
-
-**Deployment scenario.** The logger is housed in an IP67 enclosure attached to the pallet exterior. Three sealed openings: a PTFE breathable vent on the pallet-facing side wall so the SHT41 can sample the surrounding cargo-bay air; a separate 3/8-18 NPT threaded opening on the same pallet-facing wall so the PT100 probe's 150 mm sensing tip extends into the cargo airstream; and a clear polycarbonate lens window on the cargo-bay-facing wall so the inward-facing VEML7700 can detect when the cargo compartment is opened and light enters the interior. The Skylo-certified multi-band antenna (included with the NOTE-NBGLWX) is mounted inside the ABS enclosure (ABS is transparent to LTE-M and L-band NTN frequencies) with the antenna face pointing upward through the lid.
-
-**Satellite connectivity is opportunistic.** Skylo NTN uses geostationary satellites and requires a clear sky view toward the equator. When the logger is mounted on the exterior top of a pallet in open-air staging, on an open vehicle deck, or in a truck trailer with a composite (non-steel) roof, satellite connectivity is available. Inside a closed steel shipping container or enclosed trailer, satellite is blocked; Notes queue in Notecard flash and flush automatically over cellular or WiFi when the pallet reaches an area with terrestrial coverage.
-
-
-## 2. System Architecture
-
-
-
-**Device-side responsibilities.** Bolted to the pallet exterior, the Cygnet STM32L4 host on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) wakes once every five minutes and does the entire day's work in a few seconds. It reads the PT100/MAX31865 for cargo-air temperature, the SHT41 for humidity, and the VEML7700 for interior light; pulls accumulated motion events and current orientation from the Notecard's built-in accelerometer via `card.motion`; updates the shipment-state model; evaluates five threshold rule categories (temperature is two-sided — `temp_low` or `temp_high` — for six alert types in all); appends a per-sample log entry; and emits any alert or state-change Notes the rules produce before returning to sleep.
-
-**Shipment-state model.** After each motion and light read, the firmware evaluates the current shipment state:
-- **DWELL** — confirmed by `dwell_confirm_samples` consecutive low-motion samples (motion < `transit_motion_min` per interval). During dwell, both the `cargo_data.qo` summary interval and the Notecard hub.set outbound cadence are multiplied by `dwell_batch_factor` (default 4×), reducing both the number of summary notes queued per session and the number of outbound sessions per hour. `applyDynamicOutbound()` re-issues `hub.set` whenever the state transitions in or out of DWELL.
-- **IN_TRANSIT** — confirmed by `transit_confirm_samples` consecutive high-motion samples. Normal summary interval applies.
-- **HANDLING** — triggered immediately when interior lux reaches or exceeds `light_open_lux`, indicating the cargo door or container lid has been opened. Resets motion counters.
-
-When the state changes, a `cargo_state.qo` note is dispatched immediately via `sync:true` so the remote system learns about the transition in near-real-time over whatever radio is available. If the first send attempt fails (transient Notecard I²C issue), the transition is persisted in `ColdChainState` and retried on every subsequent wake until the Notecard confirms the `note.add`, so no state transition is permanently lost.
-
-**Tamper-evident local log.** Every sample cycle appends one compact-templated entry to the `cargo_log.qo` Notefile. Each entry includes a monotonic sequence number (`seq`, incremented before every note.add), a rolling integrity hash (`chain_crc`) computed over the previous hash, the sequence number, boot segment, and all sensor readings, and a `boot_seg` counter that increments on every cold boot. The boot-segment counter is persisted both in the Notecard sleep payload (planned-sleep resilience) and in a Notecard-local notefile `chain_boot.dbx` (power-loss resilience). Log entries are queued for the regular outbound window rather than synced immediately, batching with outbound sessions without consuming an extra satellite session per sample. The `_time` field is always included in each entry: the real epoch when the Notecard has obtained valid time from the [Blues Notehub](https://blues.com/notehub/) cloud service, or `0` as a documented pre-sync sentinel. Downstream consumers should treat `_time == 0` as pre-sync and use Notehub's event receive-time as the best available approximation for those records. A `motion_valid` flag (`1` = card.motion returned valid data; `0` = card.motion was unavailable) is also included in every entry so downstream consumers can distinguish "no motion occurred" (`motion = 0`, `motion_valid = 1`) from "motion data unavailable" (`motion = 0`, `motion_valid = 0`), preserving the compliance semantics of the per-sample audit log even when the accelerometer interface is temporarily unreachable. A downstream verifier replays the chain **within each `boot_seg` group** from seq=1 (seed=0); a gap in `seq` within a segment indicates a dropped transmission; a `chain_crc` mismatch indicates a modified or inserted record; and a new `boot_seg` value marks the start of a new, independent chain segment caused by a device cold boot.
-
-**Notecard responsibilities.** Notecard for Skylo holds outbound [Notes](https://dev.blues.io/api-reference/glossary/#note) in its on-device flash queue and decides for itself which radio to use (cellular, WiFi, or NTN satellite) depending on what's reachable from wherever the pallet currently sits. It flushes the queue on the configured [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) outbound cadence (default 60 minutes), and any Note marked `sync:true` jumps the queue and opens a session immediately on whatever radio is available. Coming the other way, the Notecard distributes [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) from Notehub, so shipper operations can change threshold values, sample cadence, or summary cadence mid-route without reflashing firmware.
-
-**Notehub responsibilities.** Whatever the Notecard sends — over cellular, WiFi, or NTN — lands in [Notehub](https://notehub.io), where it's stored with the original on-pallet timestamp and run through project [routes](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub). The four Notefiles (`cargo_alert.qo`, `cargo_state.qo`, `cargo_data.qo`, `cargo_log.qo`) stay separate so each stream can go to the system that wants it: alerts and state changes to the TMS or on-call channel, summaries to the cold-chain analytics platform, and the tamper-evident log entries to a compliance archive.
-
-
-## 3. Technical Summary
-
-1. **Notehub:** create a [Notehub project](https://notehub.io), copy its ProductUID.
-2. **Wire the bench rig:** Notecarrier CX + Notecard for Skylo + MAX31865 on SPI + PT100 probe + SHT41 on I²C + VEML7700 on I²C. Full pinout in [§5](#5-wiring-and-assembly).
-3. **Edit one line** in [`firmware/cargo_cold_chain_monitor/cargo_cold_chain_monitor_helpers.h`](firmware/cargo_cold_chain_monitor/cargo_cold_chain_monitor_helpers.h): set `PRODUCT_UID` to your Notehub project's value.
-4. **Flash:** run `arduino-cli board listall | grep -i cygnet` to confirm the FQBN for your installed STM32 core, then compile and upload. Full instructions in [§7.1](#71-installing-and-flashing).
-5. **Watch:** Notehub → your project → **Events** tab. You should see `_session.qo` on first contact, `cargo_data.qo` after the first summary interval, `cargo_log.qo` entries batching with each outbound sync, `cargo_state.qo` on the first confirmed dwell or motion event, and any threshold trips as `cargo_alert.qo` within one sample interval of the triggering event.
-
-> **First event timeline:** On power-up, the device acquires time and signals contact via `_session.qo` within 1–2 minutes (cellular/WiFi) or several minutes (Skylo NTN with clear sky). The first `cargo_log.qo` entry appears one sample interval later (~5 minutes). The first `cargo_data.qo` summary appears ~60 minutes after the device obtains a valid epoch from Notehub's `card.time` API.
-
-
-Here is a sample Note this device emits:
-
-```json
-{
- "_time": 1748000000,
- "temp_mean_c": 4.3,
- "temp_min_c": 3.8,
- "temp_max_c": 4.9,
- "rh_mean_pct": 62.1,
- "rh_min_pct": 60.4,
- "rh_max_pct": 63.7,
- "lux_max": 0.3,
- "motion_total": 2,
- "motion_valid": 1,
- "samples": 12
-}
-```
-
-## 4. Hardware Requirements
-
-| Part | Qty | Rationale |
-|------|-----|-----------|
-| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with embedded Cygnet STM32L4 host — no separate MCU needed. The ATTN pin controls host power for deep-sleep between samples. |
-| [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Single M.2 module with cellular (LTE-M / 2G / 3G), WiFi, and Skylo NTN satellite. Global pre-certification sidesteps per-country approval cycles. Datasheet: [dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) |
-| [Omega PR-21C-3-100-A-1/8-0600-M12-2](https://www.omega.com/en-us/temperature-measurement/temperature-probes/rtd-probes/pr-21/) PT100 Class A probe + Omega **CAL-3** NIST-traceable calibration certificate | 1 | IEC 60751 Class A PT100 (±0.15 °C at 0 °C), 4-wire, 3.2 mm (1/8″) × 150 mm (6″) 316L stainless-steel sheath with 3/8″ NPT process fitting and 4-pin M12 A-coded output connector, −50 °C to 250 °C range. **Order the probe together with Omega's CAL-3 option** — NIST-traceable calibration certificate documenting temperature deviation at standard points, performed per ISO 10012-1 / ANSI/NCSL Z540-1. The CAL-3 certificate is what makes the hardware measurement NIST-traceable; a probe shipped without it does not satisfy the calibration-documentation requirement regardless of accuracy class. The 3/8″ NPT fitting on the probe body threads into a separately drilled and tapped 3/8-18 NPT hole in the pallet-facing side wall — this is a distinct hole from the M12 PTFE vent hole (see §5). The 4-wire M12 output mates to a field-wireable M12 A-coded female connector or a pre-made M12 patch cable for clean in-enclosure routing to the MAX31865 breakout RTD terminals. Firmware uses `MAX31865_4WIRE`; the Adafruit #3328 breakout jumper must be set to the 4-wire position (desolder the default 2/3-wire bridge and close the 4-wire pads per the Adafruit product guide). |
-| [Adafruit MAX31865 RTD Amplifier Breakout (#3328)](https://www.adafruit.com/product/3328) | 1 | SPI-interface 15-bit ADC designed for PT100/PT1000 RTDs. Provides hardware fault detection (open-circuit, short-circuit, over/under-voltage) and accepts both 2-wire and 4-wire probe configurations. Connects to the Cygnet's hardware SPI bus (CS=D10, CLK=D13, SDO=D12, SDI=D11). The Adafruit #3328 includes a 430 Ω reference resistor matched to PT100 use. |
-| [Adafruit SHT41 breakout (#5776)](https://www.adafruit.com/product/5776) | 1 | Sensirion SHT41: ±1.8% RH, factory-calibrated. Used for **relative humidity only** — the MAX31865/PT100 is the primary temperature source. The SHT41's integrated heater prevents condensation-induced RH drift in cold, humid refrigerated environments. I²C, 3.3 V. |
-| [Adafruit VEML7700 Lux Sensor (#4162)](https://www.adafruit.com/product/4162) | 1 | 16-bit I²C ambient light sensor for interior cargo-bay light / door-opened detection. Mounted behind a polycarbonate lens on the **cargo-bay-facing wall** of the enclosure so it reads near-zero lux when the container or reefer is sealed and reports a significant lux reading when the cargo door is opened and light enters the compartment. A missing sensor returns `-9999` in `cargo_data.qo` and `cargo_log.qo`; the `lux` field is omitted from `cargo_alert.qo` when unavailable. |
-| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) *(bench bring-up only)* | 0–1 | Coulomb counter inline on the VBAT rail for ground-truth energy validation. Not deployed to the field. See [§9](#9-validation-and-testing). |
-| 3.7 V Li-Po battery, 2000 mAh (e.g., [Adafruit #2011](https://www.adafruit.com/product/2011)) | 1 | Portable power for multi-week shipments. JST-PH 2-pin connector mates directly to the Notecarrier CX. See [§9](#9-validation-and-testing) for runtime estimates and [§11](#11-limitations-and-next-steps) for UN 38.3 / IATA transport compliance Notes. |
-| ABS IP67 enclosure with mounting flanges | 1 | IP67-rated pallet-logger housing. Requires three enclosure openings: one M12 hole on the pallet-facing side wall for the PTFE vent plug (SHT41 air exchange only); one separately drilled and tapped 3/8-18 NPT hole on the same pallet-facing side wall for the PT100 probe entry; one 20 mm hole on the **cargo-bay-facing wall** for the VEML7700 polycarbonate lens window. ABS is easily machinable — the M12 hole accepts standard step or hole saws; the 3/8-18 NPT hole is drilled to the tap-drill size (approx. 9.5 mm / 3/8″) then threaded with a 3/8-18 NPT tap. |
-| Skylo NTN + cellular/WiFi antenna (**included with NOTE-NBGLWX**) | 1 | The NOTE-NBGLWX ships with its Skylo-certified multi-band flexible antenna, which attaches to the Notecard's `MAIN` u.FL port and covers both cellular (LTE-M) and Skylo NTN (S-Band / L-Band). **Use only the included antenna on the MAIN port.** Skylo certifies the NOTE-NBGLWX exclusively with the antenna provided in the kit. Mount inside the ABS enclosure with its face toward the sky. |
-| ePTFE breathable membrane vent plug, M12x1.5, metal, IP68/IP69K ([Amphenol LTW VENT-BJ005](https://amphenolltw.com/product-info/Vent/Vent.M12Metalic/VENT-BJ005), available at [DigiKey](https://www.digikey.com/en/products/detail/amphenol-ltw/VENT-BJ005/7102753)) | 1 | Allows the SHT41 to sample the surrounding cargo-bay air while maintaining the enclosure's IP67 seal. Installs in the dedicated M12 vent hole on the pallet-facing side wall (hand-tight + one half-turn). The PT100 probe enters through its own separate 3/8-18 NPT tapped hole on the same wall. |
-| PTFE thread sealant tape, ½ in width (e.g., Oatey 31403 or equivalent, available at any hardware store) | 1 roll | Seals the 3/8-18 NPT probe fitting into the tapped probe-entry hole in the pallet-facing enclosure wall. Apply 2–3 wraps to the probe's NPT threads before threading in. |
-| Clear polycarbonate disc, 20 mm diameter × 3 mm thick, with silicone RTV sealant ([McMaster-Carr 8560K171](https://www.mcmaster.com/8560K171/) or equivalent) | 1 | Optical window for the inward-facing VEML7700. Press into the 20 mm lens hole on the cargo-bay-facing wall and seal the perimeter with clear silicone RTV. |
-| STEMMA QT / Qwiic 4-pin JST-SH cable, 100 mm (e.g. [Adafruit #4210](https://www.adafruit.com/product/4210)) | 2 | Two cables required: one from the Notecarrier CX Qwiic port to any Qwiic port on the SHT41, and a second from the SHT41's pass-through Qwiic port to the VEML7700. |
-
-All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
-
-
-## 5. Wiring and Assembly
-
-
-
-All host I/O connects to the Notecarrier CX's dual 16-pin header. Notecard for Skylo seats into the carrier's M.2 slot; the included Skylo-certified antenna attaches to the Notecard's `MAIN` u.FL port and remains inside the ABS enclosure.
-
-**MAX31865 (SPI, temperature).**
-Connect via the Cygnet's hardware SPI bus:
-
-| MAX31865 pin | Cygnet pin | Header label |
-|---|---|---|
-| VIN | 3V3 | +3V3 |
-| GND | GND | GND |
-| CLK | D13 | SCK |
-| SDO (MISO) | D12 | MISO |
-| SDI (MOSI) | D11 | MOSI |
-| CS | D10 | CS / D10 |
-
-The specified Omega PR-21C probe has a 4-wire M12 A-coded output connector; use a field-wireable or pre-made M12 A-coded female cable to break out the four RTD leads inside the enclosure. **Set the Adafruit #3328 breakout's jumpers for 4-wire PT100:** desolder the default 2/3-wire jumper bridge and close the 4-wire pads as shown in the Adafruit product guide. Connect the four probe leads to the MAX31865's F+, F−, RTD+, and RTD− terminals. Thread the probe's 3/8-18 NPT fitting into the dedicated probe-entry hole on the pallet-facing side wall — this hole is separate from the M12 PTFE vent hole (drill to the 3/8-18 NPT tap-drill size, thread with a 3/8-18 NPT tap, apply 2–3 wraps of PTFE tape to the probe threads, and torque hand-tight plus a quarter-turn), so the 150 mm sensing tip extends into the cargo-bay airstream.
-
-**SHT41 and VEML7700 (I²C, humidity and interior light).**
-Both breakouts include STEMMA QT / Qwiic connectors. Two 100 mm STEMMA QT cables are needed: connect the first from the Notecarrier CX's Qwiic port to any Qwiic port on the SHT41, then connect the second from the SHT41's pass-through Qwiic port to the VEML7700. The Notecarrier CX has I²C pull-ups on-board; no external resistors are needed.
-
-**I²C address summary** (no conflict, both on the same bus; MAX31865 uses SPI, not I²C):
-
-| Sensor | I²C address |
-|--------|-------------|
-| SHT41 | `0x44` (fixed) |
-| VEML7700 | `0x10` (fixed) |
-
-**VEML7700 interior light path (cargo-bay-facing).**
-Drill a 20 mm through-hole in the **cargo-bay-facing wall** of the enclosure (not the lid). Press a 20 mm × 3 mm clear polycarbonate disc into the hole and seal the perimeter with clear silicone RTV; allow to cure. Mount the VEML7700 breakout directly behind the lens so the sensor die faces inward through the enclosure wall, toward the cargo space.
-
-When deployed, the cargo-bay-facing wall of the enclosure looks into the interior of the reefer compartment or container. The sealed compartment blocks daylight — the VEML7700 reads near-zero lux while the cargo door is closed. When the door opens, light enters the compartment; the sensor reads above the `light_open_lux` threshold and fires a `light_exposure` alert and HANDLING state transition. This provides reliable cargo-door-open detection at each handling event throughout the journey.
-
-**SHT41 air path (sensing through vent membrane).**
-Drill an M12 through-hole in the pallet-facing side panel for the PTFE vent plug (this hole is dedicated to the vent plug only, the PT100 probe enters through its own separate 3/8-18 NPT tapped hole on the same panel). Thread in the Amphenol WVPL-G2 vent plug (hand-tight + one half-turn). Position the SHT41 breakout inside the enclosure with its sensor opening within 10 mm of the vent plug's inner face so the airflow path reaches the sensor die.
-
-**Additional wiring:**
-- **+VBAT** → Mojo `LOAD` output (bench only). In field deployment, connect the Li-Po directly to the Notecarrier CX's JST-PH LiPo jack.
-
-**Antenna placement (internal).** Attach the included Skylo-certified antenna to the Notecard's `MAIN` u.FL port. Mount the antenna inside the ABS enclosure with its face pointing upward toward the lid to maximize sky-facing aperture for Skylo NTN. **Do not substitute a different antenna on the MAIN port.** The `GPS` u.FL port is not connected in this project.
-
-> **No external accelerometer required.** Notecard for Skylo includes a built-in 3-axis accelerometer. Motion tracking is enabled via `card.motion.mode` with `start:true` and `sensitivity:2`; the firmware does not configure sample rate or G-range — those are managed internally by the Notecard. The `card.motion` stream provides per-bucket motion counts for shock detection and the `orientation` field for tilt detection.
-
-
-## 6. Notehub Setup
-
-1. **Create a project.** Sign up at [notehub.io](https://notehub.io) and create a project. Copy the [ProductUID](https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid) — it looks like `com.your-company.your-name:cold-chain`.
-
-2. **Set the ProductUID in firmware.** Open [`firmware/cargo_cold_chain_monitor/cargo_cold_chain_monitor_helpers.h`](firmware/cargo_cold_chain_monitor/cargo_cold_chain_monitor_helpers.h) and replace the empty string on the `#define PRODUCT_UID ""` line with your value.
-
-3. **Claim the Notecard.** Power the assembled unit. The Notecard associates itself with your Notehub project on the **first successful radio session over any available RAT** — cellular, WiFi, or Skylo NTN. The device appears in your project's **Devices** tab after that first session completes. Over cellular or WiFi in good coverage, this typically happens within a minute or two of power-on. Over Skylo NTN, first contact requires a clear, unobstructed view toward the equatorial sky; session acquisition can take several minutes to establish, and is not possible inside enclosed metal structures or below grade. The Notecard MUST sync over cellular or WiFi once prior to NTN.
-
-> **WiFi provisioning (optional).** Notecard for Skylo treats WiFi as its highest-priority radio when credentials are present, but credentials are **not** provisioned automatically. To provision WiFi, send a [`card.wifi`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-wifi) request to the Notecard via the in-browser Notecard terminal or the [Notecard CLI](https://dev.blues.io/tools-and-sdks/notecard-cli/):
-> ```json
-> { "req": "card.wifi", "ssid": "your-network-ssid", "password": "your-network-password" }
-> ```
-> Devices deployed without pre-provisioned WiFi credentials operate on cellular and satellite only — the normal mode for road and ocean transit segments.
-
-4. **Create a Fleet per lane or customer.** [Fleets](https://dev.blues.io/guides-and-tutorials/fleet-admin-guide/) group devices for shared configuration. A natural model: one fleet per customer or product type (e.g., `insulin-2-8c` vs `fresh-produce`). [Smart Fleets](https://dev.blues.io/notehub/notehub-walkthrough/#using-smart-fleet-rules) can auto-assign devices based on a serial-number prefix so each new logger automatically inherits the right thresholds on first appearance.
-
-5. **Set environment variables.** In Notehub, navigate to **Fleet → Environment** (or **Device → Environment** for per-device overrides). All variables are optional; firmware defaults are shown.
-
- > **How env-var delivery works.** `env.get` reads the Notecard's *locally cached* copy on every wake — a value that has already been delivered takes effect immediately on the next sample cycle. New or changed values set in the Notehub console only reach the device after the next *inbound sync*. The default inbound cadence is `INBOUND_INTERVAL_MIN = 720` (12 hours). During commissioning, temporarily force a sync by clicking **Sync** on the device's detail page, or reduce `INBOUND_INTERVAL_MIN` to 60 for the bench session and restore before field deployment.
-
- | Variable | Default | Purpose |
- |---|---|---|
- | `temp_min_c` | `2.0` | Lower temperature limit (°C). Standard pharma cold chain; fresh produce lanes may differ. |
- | `temp_max_c` | `8.0` | Upper temperature limit (°C). `temp_high` fires when exceeded; `temp_low` fires below `temp_min_c`. |
- | `humidity_max_pct` | `75.0` | Relative humidity ceiling (%). High RH in a refrigerated environment signals condensation risk or reefer malfunction. |
- | `light_open_lux` | `50.0` | Interior lux above which `light_exposure` fires and the state transitions to HANDLING. The interior of a sealed reefer or container reads near 0 lux; 50 lux reliably detects door opening. |
- | `shock_events` | `5` | Accumulated motion-event count per sample interval above which `shock_detected` fires. Lower values catch lighter impacts; raise to 10–15 for lanes with expected road vibration. |
- | `sample_interval_sec` | `300` | Seconds between sample cycles (minimum 60). Reducing to 60 for bench testing is fine. |
- | `summary_interval_min` | `60` | Base minutes between `cargo_data.qo` summary Notes. Extended by `dwell_batch_factor` during confirmed DWELL. Controls summary generation cadence; outbound sync interval is the compiled `OUTBOUND_INTERVAL_MIN` constant (default 60 minutes). |
- | `transit_motion_min` | `3` | Motion events per sample interval at or above which the sample counts as "moving." Tune up for noisy lanes (vibration-heavy roads). |
- | `dwell_confirm_samples` | `3` | Consecutive low-motion samples before the state transitions to DWELL (~15 minutes at default cadence). |
- | `transit_confirm_samples` | `2` | Consecutive high-motion samples before the state transitions to IN_TRANSIT (~10 minutes at default cadence). |
- | `dwell_batch_factor` | `4` | Multiplier applied to both `summary_interval_min` and the hub.set outbound cadence during confirmed DWELL. Default 4 → 4-hour summary intervals **and** 4-hour outbound sync windows in a warehouse (9 sessions per 36-hour dwell instead of 36). Alert and state-change Notes (sync:true) still trigger an immediate session regardless. Set to 1 to disable dwell batching entirely. |
-
-6. **Configure routes.** Add one [route](https://dev.blues.io/notehub/notehub-walkthrough/#routing-data-with-notehub) for `cargo_alert.qo` and `cargo_state.qo` (to a TMS webhook or on-call endpoint), one for `cargo_data.qo` (to a cold-chain analytics platform), and one for `cargo_log.qo` (to a compliance data store or audit system). Keeping the four Notefiles separate at the source means routing policy is set once and downstream systems receive only the event streams they need.
-
-### What to expect in Notehub
-
-Within a short time of first power-on the **Events** tab begins populating. Timing depends on available RATs. See step 3 above.
-
-- **`_session.qo`** — automatic Notecard housekeeping on each radio session. Useful first-light sanity check.
-
-- **`cargo_data.qo`** — one per effective summary interval (60 minutes in transit/handling, up to 240 minutes during DWELL at default settings), starting after the first complete interval elapses from anchor time. Body:
- ```json
- {
- "_time": 1748000000,
- "temp_mean_c": 4.3,
- "temp_min_c": 3.8,
- "temp_max_c": 4.9,
- "rh_mean_pct": 62.1,
- "rh_min_pct": 60.4,
- "rh_max_pct": 63.7,
- "lux_max": 0.3,
- "motion_total": 2,
- "motion_valid": 1,
- "samples": 12
- }
- ```
- Any field reading `-9999` means no valid sensor data was available for that metric in the window — treat as a sensor fault, not a near-zero measurement.
-
-- **`cargo_log.qo`** — one compact entry per sample cycle, batched with the regular outbound sync. Body:
- ```json
- {
- "_time": 1748000300,
- "seq": 42,
- "temp_c": 4.3,
- "rh_pct": 62.5,
- "lux": 0.2,
- "motion": 0,
- "motion_valid": 1,
- "state": 1,
- "boot_seg": 1,
- "chain_crc": 3748291045
- }
- ```
- `_time` is always present. Entries logged before the Notecard obtained a valid epoch carry `_time = 0` as a pre-sync sentinel; use Notehub's event receive-time as the approximation for those records. `motion_valid` is `1` when `card.motion` returned valid data and `0` when the accelerometer interface was unavailable — a value of `0` with `motion_valid = 0` means data was unavailable, not that no motion occurred. `state` maps to: `0`=unknown, `1`=dwell, `2`=in_transit, `3`=handling. `boot_seg` increments on every cold boot — all entries with the same `boot_seg` form one continuous chain segment. A gap in `seq` within a segment indicates a missed entry. A `chain_crc` mismatch (when replaying the chain within one `boot_seg` from seq=1, seed=0) indicates a modified or inserted record. A new `boot_seg` value resets seq to 1 and starts a fresh chain from seed=0.
-
-- **`cargo_state.qo`** — emitted on every shipment-state transition, transmitted immediately. Body:
- ```json
- {
- "_time": 1748001200,
- "state_from": "dwell",
- "state_to": "in_transit"
- }
- ```
-
-- **`cargo_alert.qo`** — emitted only on a threshold trip, transmitted immediately. The `alert` field is one of `temp_low`, `temp_high`, `humidity_high`, `shock_detected`, `light_exposure`, or `tilt_detected`. Standard alert:
- ```json
- {
- "alert": "temp_high",
- "temp_c": 9.1,
- "rh_pct": 64.2,
- "lux": 0.1,
- "motion": 0
- }
- ```
- Tilt alerts add `orientation_from` and `orientation_to`. If a sensor was unavailable when the alert fired, its field is absent — an omitted field indicates a sensor fault; `-9999` is used only in `cargo_data.qo` and `cargo_log.qo`.
-
-
-## 7. Firmware Design
-
-Single sketch: [`firmware/cargo_cold_chain_monitor/cargo_cold_chain_monitor.ino`](firmware/cargo_cold_chain_monitor/cargo_cold_chain_monitor.ino).
-
-### 7.1 Installing and flashing
-
-**Dependencies:**
-
-- **Arduino core for STM32** — [`stm32duino/Arduino_Core_STM32`](https://github.com/stm32duino/Arduino_Core_STM32). Install via the Arduino Boards Manager by adding the index URL `https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json` under **File → Preferences → Additional Boards Manager URLs** and searching **STM32 MCU based boards**.
-- **`Blues Wireless Notecard`** library ([`note-arduino`](https://github.com/blues/note-arduino)) — install via `arduino-cli lib install "Blues Wireless Notecard"` or search "Blues Wireless Notecard" in the Arduino IDE Library Manager.
-- **`Adafruit MAX31865 library`** — install via Library Manager (search "Adafruit MAX31865").
-- **`Adafruit SHT4x Library`** — install via Library Manager (search "Adafruit SHT4x").
-- **`Adafruit VEML7700 Library`** — install via Library Manager (search "Adafruit VEML7700").
-
-**Flashing — `arduino-cli`:** run from the repo root:
-
-```bash
-# Step 1: find the exact FQBN for the Cygnet on your installed core version
-arduino-cli board listall | grep -i cygnet
-
-# Step 2: compile (replace the FQBN below with what Step 1 reported)
-arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- firmware/cargo_cold_chain_monitor/
-
-# Step 3: upload (replace the FQBN below with what Step 1 reported)
-arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
- -p /dev/cu.usbmodem* firmware/cargo_cold_chain_monitor/
-```
-
-> The FQBN shown in Steps 2–3 (`STMicroelectronics:stm32:Blues:pnum=CYGNET`) is the value typically reported by current `stm32duino` core releases, but the authoritative value for your specific installed core is whatever `arduino-cli board listall | grep -i cygnet` prints — always substitute that output before running compile or upload. Replace `/dev/cu.usbmodem*` with your actual port (`COMx` on Windows, `/dev/ttyACM*` on Linux).
-
-Open the serial monitor at **115200 baud** to watch `[cargo]` log lines. On the first cold boot you'll see Notecard configuration messages, then one `[cargo] T=X.XX C (PT100)` and `[cargo] RH=XX.X %` per wake. After the first summary interval you'll see `[cargo] summary sent — samples=N`.
-
-### 7.2 Modules
-
-| Responsibility | Where in code |
-|---|---|
-| Notecard configuration (`hub.set`, motion mode) with warm-boot retry | `notecardConfigure()` + flags in `ColdChainState` |
-| Compact template definition for `cargo_data.qo` and `cargo_log.qo` | `defineTemplates()` + `templates_registered` flag |
-| Environment-variable fetch, clamp, and cadence-change window reset | `fetchEnvOverrides()`, `envFloat()` helper |
-| PT100/MAX31865 temperature reads (SPI) | `readSensors()` |
-| SHT41 relative humidity reads (I²C, humidity channel only) | `readSensors()` |
-| VEML7700 interior cargo-bay light reads | `readSensors()` |
-| Notecard built-in accelerometer motion count + orientation | `readMotionCount()` |
-| Shipment-state detection (DWELL / IN_TRANSIT / HANDLING) | `detectShipmentState()` |
-| State-transition immediate-sync Note | `sendStateChange()` |
-| Tamper-evident per-sample log entry (seq, chain_crc) | `sendLogEntry()` |
-| Alert cooldown logic | `alertCooldownOk()` |
-| Threshold evaluation and alert dispatch | `evaluateAlerts()`, `sendAlert()`, `sendTiltAlert()` |
-| Rolling sample accumulation | `accumulateSample()` |
-| Adaptive summary interval (dwell batching), snapshot/retry/discard | `snapshotSummary()`, `sendPendingSummary()`, `resetAccumulators()` |
-| Persistent state across sleep cycles | `ColdChainState` struct + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
-| Epoch time for cooldowns and timestamps | `currentEpoch()` |
-
-### 7.3 Sensor reading strategy
-
-- **MAX31865 / PT100.** `rtd.begin(MAX31865_4WIRE)` re-initializes the SPI peripheral on every wake (the device is re-powered with the host) in 4-wire mode to match the specified Omega PR-21C probe. 4-wire mode drives force current through the outer pair of leads and measures voltage across the inner pair, eliminating lead-resistance error; this matters for probe cable runs over 0.5 m. `rtd.temperature(MAX31865_RNOMINAL, MAX31865_RREF)` reads the RTD resistance and applies the Callendar–Van Dusen polynomial. `rtd.readFault()` checks for open-circuit (RTDINLOW), short-circuit (HIGHTHRESH), and reference-voltage faults; any non-zero fault sets `temp_c = INVALID_F` and clears the fault register so the next wake gets a fresh read. Valid temperature values are range-checked (−200 °C to +200 °C) before acceptance. The 430 Ω reference resistor on the Adafruit #3328 breakout matches the PT100 nominal range; the `MAX31865_RREF` and `MAX31865_RNOMINAL` constants in `helpers.h` must be updated if a PT1000 probe is substituted.
-- **SHT41 (humidity only).** `setPrecision(SHT4X_HIGH_PRECISION)` selects the ±1.8% RH mode. `getEvent()` returns both a temperature and a humidity struct; only the humidity channel is used — the PT100/MAX31865 is the authoritative temperature source. NaN guards are applied before accumulation.
-- **VEML7700.** Gain `VEML7700_GAIN_2` and integration time `VEML7700_IT_100MS` maximize sensitivity for the near-zero lux levels expected inside a sealed reefer or container. A missing VEML7700 returns `INVALID_F` (not 0.0) so a sensor fault is distinguishable from genuine darkness. The `light_exposure` alert and HANDLING state detection both skip when lux is `INVALID_F`.
-- **Accelerometer.** `card.motion` is called with `minutes: gSampleSec / 60` to retrieve a non-overlapping window covering exactly the elapsed sample interval. `sample_interval_sec` is always clamped to whole-minute multiples in env-var processing so the division is exact. Per-bucket motion counts are parsed in-place with `strtoul` pointer arithmetic so multi-digit counts (e.g., `"10"`) are handled correctly for any window length.
-
-### 7.4 Event payload design
-
-**`cargo_data.qo`** — adaptive cadence, compact-templated. Notehub compact templates (registered in code at [§7.7](#77-key-code-snippet-1-compact-log-template) for `cargo_data.qo` and §7.7 for `cargo_log.qo`) reduce on-wire size from ~200 bytes (free JSON) to ~50 bytes per message, which meaningfully reduces satellite session overhead. During confirmed DWELL the effective interval is `summary_interval_min × dwell_batch_factor` (default 4 hours); during IN_TRANSIT and HANDLING the base `summary_interval_min` (default 60 minutes) applies. The `_time` field is preserved in the compact body so each record carries its own audit timestamp independent of Notehub's receive-time metadata.
-
-**`cargo_log.qo`** — one entry per sample cycle, compact-templated on Notehub port 51 (defined at [§7.7](#77-key-code-snippet-1-compact-log-template)). Entries are queued for the regular outbound window, with no extra satellite session per sample. Each entry carries `seq` (monotonic counter, incremented before every Note.add), `boot_seg` (cold-boot counter, incremented and persisted to `chain_boot.dbx` on every cold boot), and `chain_crc` (rolling hash computed per [§7.8](#78-key-code-snippet-2-integrity-chain-hash-update) over previous hash + seq + boot_seg + all sensor fields). The `_time` field is always included in each entry: the real sample epoch when valid time is available, or `0` as a documented pre-sync sentinel when the Notecard has not yet obtained time from Notehub. Writing `0` rather than omitting the field keeps the compact-template body consistent with the registered schema; downstream consumers should treat `_time == 0` as pre-sync and use Notehub's receive-time as the best available approximation for those entries. The `motion_valid` field (`1` or `0`) distinguishes a genuine zero-motion reading from a cycle where `card.motion` was unavailable — both cases store `motion = 0`, but `motion_valid = 0` signals missing data rather than confirmed stillness. The chain hash uses the stored `motion` value (which is `0` when `card.motion` is unavailable), so downstream replay is straightforward from the logged fields. A downstream verifier first groups entries by `boot_seg`, then replays the chain within each group using the algorithm at [§7.8](#78-key-code-snippet-2-integrity-chain-hash-update): `crc[n] = hash(crc[n-1], seq[n], boot_seg[n], temp[n], rh[n], lux[n], motion[n], state[n])`, starting from `crc[0]=0`. A seq gap within a group indicates a missed transmission; a chain mismatch indicates a modified or inserted record; a new `boot_seg` value marks a cold-boot boundary and starts a fresh chain from seed=0.
-
-**`cargo_state.qo`** — on any DWELL / IN_TRANSIT / HANDLING state transition, `sync:true`, free-form JSON. Two fields: `state_from` and `state_to` (string names), plus `_time` when valid epoch is available. Provides a near-real-time chain-of-custody record of when the shipment began and ended each handling and transit event.
-
-**`cargo_alert.qo`** — on threshold trip, `sync:true`, free-form JSON. Alert Notes are infrequent and not templated.
-
-### 7.5 Low-power and satellite strategy
-
-The host is fully powered off between samples via `NotePayloadSaveAndSleep` / `card.attn`. The entire `ColdChainState` struct, including `seq`, `chain_crc`, `boot_seg`, `last_outbound_min`, shipment-state fields, alert cooldowns, and summary window accumulators — is serialized to Notecard flash before each sleep so all state survives planned sleep/wake cycles.
-
-**Uncontrolled cold boot behavior.** A battery disconnection, brown-out, or deliberate reset before `NotePayloadSaveAndSleep` completes causes `NotePayloadRetrieveAfterSleep` to return `warmBoot=false` on the next power-on. `gState` is zeroed and `loadOrIncrementBootSeg()` reads the `boot_seg` counter **and the tilt baseline orientation** from `chain_boot.dbx` (a Notecard-local notefile that survives host power loss), increments the counter, and writes both back. Restoring `baseline_orientation` from `chain_boot.dbx` ensures that tilt detection after an uncontrolled cold boot continues comparing against the orientation captured at logger activation, not against the post-boot orientation — preventing a power loss during transit from silently resetting the baseline and suppressing a real tilt event. The first time the baseline is set (true first activation) it is saved to `chain_boot.dbx` by `persistBaselineOrientation()`; every subsequent cold boot reads it back automatically. `seq` and `chain_crc` reset to 0, starting a new independent chain segment. In-progress summary accumulators and cooldown timestamps from the interrupted cycle are lost; the remote log shows a new `boot_seg` value at the first entry after the reboot, providing a clear segment boundary. Downstream verifiers should treat each `boot_seg` as an independent chain.
-
-For satellite efficiency: compact template format on `cargo_data.qo` and `cargo_log.qo` minimizes per-Note byte count; the 12-hour inbound interval (`INBOUND_INTERVAL_MIN = 720`) limits NTN inbound poll cost (~50 bytes per poll); and dwell-period batching (4× by default) extends **both** the summary generation interval and the Notecard outbound sync cadence via `applyDynamicOutbound()`, directly reducing the number of outbound NTN sessions during long warehouse stays. Alert and state-change Notes (sync:true) always trigger an immediate session regardless of the configured outbound cadence.
-
-### 7.6 Retry and error handling
-
-- `hub.set` is re-issued on every warm boot (idempotent). `card.motion.mode` and both `note.template` registrations each set a flag in `ColdChainState` on success and are retried until confirmed. All three steps are reapplied when `CONFIG_VERSION` changes (SCHEMA_VERSION = 5 encodes the current template schema, including the `motion_valid` field added to `cargo_log.qo` in schema version 5).
-- Alert cooldown timestamps advance only when `note.add` is confirmed by the Notecard; a transient failure leaves the cooldown state unchanged so the next wake retries.
-- `seq` and `chain_crc` advance before the `note.add` call in `sendLogEntry()`, so the chain represents the physical event sequence. A failed transmission burns the sequence number; the resulting gap in the remote log is itself evidence of a dropped record.
-- State-change retry: a `cargo_state.qo` `note.add` that fails is stored in `ColdChainState.pending_state_change` / `pending_state_*` fields and retried on every subsequent wake before new state detection runs. This guarantees that no state transition is permanently lost on a transient Notecard failure. Retry is attempted in chronological order: the pending transition is sent before any new transition is stored, so chain-of-custody records arrive in sequence.
-- Summary retry: a failed `sendPendingSummary()` leaves `pending_epoch` set; the frozen snapshot is retried on every subsequent wake. If the Notecard is unreachable for a full additional summary window, the stale snapshot is discarded (logged as a warning) and replaced by the newly completed window.
-
-### 7.7 Key code snippet 1: compact log template
-
-```cpp
-J *req = notecard.newRequest("note.template");
-JAddStringToObject(req, "file", NOTE_LOG); // "cargo_log.qo"
-JAddNumberToObject(req, "port", 51);
-JAddStringToObject(req, "format", "compact");
-J *body = JAddObjectToObject(req, "body");
-JAddNumberToObject(body, "_time", TUINT32); // sample epoch; 0 = pre-sync sentinel
-JAddNumberToObject(body, "seq", TUINT32); // monotonic counter (resets each boot_seg)
-JAddNumberToObject(body, "temp_c", TFLOAT32); // PT100 temperature
-JAddNumberToObject(body, "rh_pct", TFLOAT32); // relative humidity
-JAddNumberToObject(body, "lux", TFLOAT32); // interior lux
-JAddNumberToObject(body, "motion", TUINT32); // motion events (0 when motion_valid=0)
-JAddNumberToObject(body, "motion_valid", TUINT16); // 1 = card.motion available; 0 = unavailable
-JAddNumberToObject(body, "state", TUINT16); // shipment state
-JAddNumberToObject(body, "boot_seg", TUINT16); // cold-boot segment counter
-JAddNumberToObject(body, "chain_crc", TUINT32); // integrity chain hash (per boot_seg)
-ncSend(req);
-```
-
-### 7.8 Key code snippet 2: integrity chain hash update
-
-```cpp
-// chainUpdate: mix previous hash with current sample fields to produce a new
-// hash. boot_seg is included so chains from different boot segments are
-// cryptographically distinct even if seq and sensor values happen to coincide.
-// Downstream consumers replay within each boot_seg from seq=1, seed=0 to
-// verify no records were inserted, deleted, or modified in the remote log.
-static uint32_t chainUpdate(uint32_t prev, uint32_t seq, uint16_t boot_seg,
- float temp_c, float rh_pct, float lux,
- uint32_t motion, uint8_t state) {
- #define ROTMIX(h, v) ((h) = (((h) << 5) | ((h) >> 27)) ^ (v))
- uint32_t h = prev ^ 0x5A827999UL;
- ROTMIX(h, seq);
- ROTMIX(h, (uint32_t)boot_seg);
- uint32_t bits;
- memcpy(&bits, &temp_c, sizeof(bits)); ROTMIX(h, bits);
- memcpy(&bits, &rh_pct, sizeof(bits)); ROTMIX(h, bits);
- memcpy(&bits, &lux, sizeof(bits)); ROTMIX(h, bits);
- ROTMIX(h, motion);
- ROTMIX(h, (uint32_t)state);
- #undef ROTMIX
- return h;
-}
-```
-
-### 7.9 Key code snippet 3: shipment-state detection and adaptive batching
-
-```cpp
-// After sensor reads, detect state and emit state-change note if needed:
-uint8_t prevState = gState.shipment_state;
-if (detectShipmentState(motion, motionOk, lux, now)) {
- sendStateChange(prevState, gState.shipment_state, now);
-}
-
-// Adaptive summary interval — extended during confirmed dwell:
-uint32_t effectiveSummaryMin = gSummaryMin;
-if (gState.shipment_state == SHIP_STATE_DWELL) {
- uint32_t extended = gSummaryMin * gDwellBatchFactor;
- effectiveSummaryMin = (extended > 1440U) ? 1440U : extended;
-}
-// Use effectiveSummaryMin in the intervalElapsed check below.
-```
-
-### 7.10 Key code snippet 4: sleep with persistent state
-
-```cpp
-NotePayloadDesc save = {0, 0, 0};
-NotePayloadAddSegment(&save, STATE_SEG, &gState, sizeof(gState)); // STATE_SEG = "COL6"
-NotePayloadSaveAndSleep(&save, gSampleSec, NULL);
-// Host power is cut here — execution resumes at setup() on the next wake
-```
-
-
-## 8. Data Flow
-
-
-
-Every five minutes the firmware wakes, reads all sensors and the accelerometer, updates the shipment-state model, evaluates threshold rules, writes a per-sample log entry, and either queues the readings for the next summary or emits an alert Note, or both.
-
-**Shipment-state model.** The firmware tracks three confirmed states (DWELL, IN_TRANSIT, HANDLING) derived from consecutive motion counts and interior lux. DWELL transitions coarsen the summary interval by `dwell_batch_factor`; IN_TRANSIT and HANDLING use the base `summary_interval_min`. Any state transition emits a `cargo_state.qo` Note immediately via `sync:true`. This means:
-- A pallet sitting at a DC for 36 hours generates summary Notes every 4 hours (default) and syncs outbound only every 4 hours: 9 summaries and 9 outbound sessions instead of 36 each, saving NTN data budget during the long dwell. Alert and state-change Notes (sync:true) still trigger an immediate session at any point.
-- After `transit_confirm_samples` consecutive high-motion sample intervals (two at the default setting, approximately 10 minutes at the default 5-minute cadence), the state transitions to IN_TRANSIT and a state-change Note fires immediately so the remote system knows the shipment is moving.
-- When the cargo door opens, the state transitions to HANDLING and a `light_exposure` alert fires simultaneously.
-
-**Collected.** Per 5-minute cycle: temperature (°C, from PT100/MAX31865), relative humidity (%, from SHT41), interior lux (from VEML7700), accumulated motion-event count, current orientation string, and shipment state.
-
-**Transmitted.**
-- `cargo_log.qo` — one compact entry per sample cycle, batched with the regular outbound sync (not immediate). Each entry: `_time` (real epoch or `0` for pre-sync), `seq`, `temp_c`, `rh_pct`, `lux`, `motion`, `motion_valid`, `state`, `boot_seg`, `chain_crc`.
-- `cargo_data.qo` — one compact summary per effective summary interval (hourly in transit; batched during dwell). Mean/min/max for temperature and humidity, peak lux, total motion events, `motion_valid` flag, and sample count.
-- `cargo_state.qo` — emitted on every DWELL / IN_TRANSIT / HANDLING state transition, `sync:true`.
-- `cargo_alert.qo` — emitted only on a threshold trip, `sync:true`.
-
-**Routed.** Notehub fans each Notefile to its configured destination. The four files are separate at the source so routing policy is set once: `cargo_log.qo` and `cargo_data.qo` to a compliance data store and analytics platform; `cargo_alert.qo` and `cargo_state.qo` to real-time endpoints (TMS, on-call webhook, SMS gateway).
-
-**Alerts trigger on:**
-- `temp_low` — temperature below `temp_min_c` (default 2.0 °C). PT100 measurement.
-- `temp_high` — temperature above `temp_max_c` (default 8.0 °C). PT100 measurement.
-- `humidity_high` — relative humidity above `humidity_max_pct` (default 75%).
-- `shock_detected` — accumulated motion-event count in the sample window ≥ `shock_events` (default 5). Heuristic event-count threshold, not calibrated peak-G.
-- `light_exposure` — interior lux ≥ `light_open_lux` (default 50 lux). Fires when the cargo door or container lid is opened and light enters the cargo space. Also triggers the HANDLING state transition.
-- `tilt_detected` — current orientation differs from the baseline captured at logger activation. Detects a pallet tipped on its side. Adds `orientation_from` and `orientation_to` fields to the standard alert body.
-
-Each alert type has its own 30-minute cooldown (`ALERT_COOLDOWN_SEC`). A sustained temperature excursion during a 6-hour transit generates at most 12 alerts per type — enough to document the event without flooding an on-call queue.
-
-
-## 9. Validation and Testing
-
-**Expected steady-state cadence.** In normal refrigerated transit (IN_TRANSIT state), a correctly-behaving unit generates one `cargo_data.qo` summary per hour, one `cargo_log.qo` entry per 5-minute sample cycle (batched into hourly outbound syncs), and zero `cargo_alert.qo` events. During confirmed DWELL the summary cadence coarsens to one per 4 hours (default). The first summary appears approximately `summary_interval_min` minutes after the unit first obtains a valid epoch from `card.time`.
-
-**Using Mojo for bench power validation.** Connect the [Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) inline between the Li-Po battery and the Notecarrier CX +VBAT rail for bench bring-up.
-
-The figures in the table below are from two different sources — the boundary is noted:
-
-| Phase | Radio | Source | Expected |
-|---|---|---|---|
-| Host + sensors active (~5–10 seconds per wake) | n/a | Full-system estimate | 15–30 mA |
-| Notecard idle (between radio sessions) | n/a | NOTE-NBGLWX datasheet | < 0.1 mA total system |
-| Hourly outbound sync, small Note batch | LTE-M / 2G | Full-system estimate | 150–300 mA average, 15–45 s burst |
-| Hourly outbound sync, small Note batch | WiFi | Full-system estimate | 80–150 mA average, 5–20 s burst |
-| Satellite session (NTN) | Skylo NTN | Full-system estimate | 250–450 mA during acquisition + transfer; acquisition 1–5 min with clear sky; not available inside enclosed metal or below grade |
-
-> **Notecard datasheet figures:** authoritative idle and peak-session current for the NOTE-NBGLWX are published in the [NOTE-NBGLWX datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/). The Skylo NTN power profile differs meaningfully from standard cellular Notecards; consult the datasheet for the authoritative per-mode numbers. The table rows labeled "full-system estimate" add Cygnet host and sensor draws based on typical STM32L4 and sensor datasheet values and should be treated as order-of-magnitude guidance, not published specifications.
-
-**Battery-life estimates (full-system, cellular/WiFi-dominant lanes).**
-At the default 5-min sample / 1-hr cellular sync cadence on a 2 000 mAh battery:
-- 12 host wakes/hour × ~7 seconds × 20 mA ≈ 0.47 mAh/hour
-- 1 hourly LTE-M sync × 30 seconds avg × 250 mA ≈ 2.1 mAh/hour
-- Notecard idle overhead ≈ 0.02 mAh/hour
-- **Total: ~2.6–3.5 mAh/hour consumed → ~600–840 mAh consumed per day → approximately 24–32 days of operation**
-
-**Battery-life estimates (satellite-heavy lanes).**
-Each Skylo NTN session is significantly more energy-intensive than a cellular session. Conservative estimate for a session including acquisition (~150 seconds at ~350 mA average):
-- Per NTN sync session: ~14–15 mAh
-- 50% of hourly syncs over NTN (12 syncs/day, 6 over NTN): ~8–9 mAh/hour average radio → **~9–12 days on 2 000 mAh**
-- All syncs over NTN (12 syncs/day over NTN): ~15–16 mAh/hour → **~5–6 days**
-
-> **Improving satellite endurance:** The `dwell_batch_factor` automatically extends the outbound sync cadence during confirmed warehouse dwell without a reflash. During a typical pharma shipment (3–5 days in transit, 24–36 hours in warehouses), dwell batching can reduce total NTN sessions by 50–70%, extending battery life significantly. For routes with extended dwell periods, set `dwell_batch_factor` to 6 or 8 instead of the default 4.
-
-For satellite-reliant lanes, the primary lever is `dwell_batch_factor` — it dynamically extends the outbound sync cadence during warehouse dwell without a reflash, directly cutting the number of NTN sessions during the longest segments of a typical shipment. Reducing `OUTBOUND_INTERVAL_MIN` (compile-time constant, requires reflash) gives a blanket reduction regardless of state. The `cargo_log.qo` entries add modest data volume (~30 bytes/entry × 12 entries/hour = ~360 bytes/hour in transit; batched into fewer, larger sessions during dwell) that are transmitted in the same outbound sessions as summary Notes, not in separate sessions.
-
-**Healthy Mojo trace pattern:**
-- Flat near-zero baseline, brief blips at the sample interval (host active), one radio burst per outbound sync.
-- **Failure mode A:** flat continuous baseline at 15–30 mA → host never sleeping; `card.attn` not cutting power.
-- **Failure mode B:** hourly bursts running 5–10 minutes → radio struggling, retrying on weak signal.
-
-**Functional smoke tests:**
-
-> **Env-var prerequisite.** After changing an environment variable in the Notehub console, force delivery by clicking **Sync** on the device's detail page, or wait for the next inbound sync (up to 12 hours at the default cadence).
-
-- *Temperature alert:* Set `temp_max_c` to `-1.0` and sync. On the next wake (within `sample_interval_sec` seconds) the room-temperature bench exceeds the threshold and emits `cargo_alert.qo` with `alert=temp_high`. Reset when done.
-- *Light-exposure / door-open alert:* Set `light_open_lux` to `1.0` and sync. Shine a flashlight through the cargo-bay-facing lens window. Confirm `cargo_alert.qo` with `alert=light_exposure` and `cargo_state.qo` with `state_to=handling` on the next wake.
-- *Shock alert:* Set `shock_events` to `2` and sync. Tap the assembly firmly. Confirm `shock_detected` alert on the next wake.
-- *Tilt alert:* Allow the baseline orientation to be set (visible as `[cargo] orientation baseline set: face-up` on the serial monitor). Rotate the assembly. Confirm `tilt_detected` alert with `orientation_from` and `orientation_to` fields.
-- *Dwell → in-transit transition:* Leave the unit stationary for ≥ 3 sample cycles (`dwell_confirm_samples` default). Confirm `cargo_state.qo` with `state_to=dwell`. Then move the unit briskly for 2 sample cycles. Confirm `cargo_state.qo` with `state_to=in_transit`.
-- *Chain integrity:* Collect a sequence of `cargo_log.qo` entries from Notehub. Group them by `boot_seg`. Within each group, replay the chain hash from seq=1, seed=0 using the same algorithm (`chainUpdate` with `boot_seg` as a parameter) against the recorded field values. Confirm the final `chain_crc` in each group matches the last entry. A new `boot_seg` value resets the replay to seed=0.
-
-
-## 10. Troubleshooting
-
-| Symptom | Likely Cause | Resolution |
-|---------|--------------|-----------|
-| **Serial monitor shows `[cargo] I2C error:...` on cold boot** | MAX31865 or Notecard I²C race on startup; transient, not persistent. | Normal on first boot. Observe a few seconds; should clear by second sample cycle. If persistent, check SPI/I²C pin connections. |
-| **Device does not appear in Notehub after 5 minutes** | No radio available; dead zone or antenna issue. | Check antenna is attached to Notecard `MAIN` u.FL port (not GPS). If cellular available, try manual sync: `card.transport` API via terminal. Skylo NTN requires clear sky — move unit outside. |
-| **Serial shows `PRODUCT_UID is not defined` pragma warning** | Firmware was not reflashed after editing `helpers.h`. | Re-run compile and upload steps in [§7.1](#71-installing-and-flashing). |
-| **`cargo_log.qo` entries show `_time=0` forever** | Device has not obtained a valid epoch from Notehub. | This is the "pre-sync sentinel" — documented behavior. After the first inbound sync, entries will carry real epoch. Check Notehub → Device → Environment to confirm inbound cadence (default 12 hours). Force immediate sync by clicking **Sync** on the device detail page. |
-| **No `cargo_alert.qo` entries despite obvious threshold breach** | Alert type already fired within the last 30 minutes; cooldown active. | This is by design — cooldowns prevent repeated alerts during sustained excursions. Check the `cargo_log.qo` per-sample entries and `cargo_data.qo` summaries instead; they show the full condition history. |
-| **Battery drains in < 7 days (cellular lanes)** | Continuous motion is preventing DWELL batching, or outbound sync is too frequent. | Check `hub.set` config in firmware logs. If device is in a vibration-heavy location, raise `transit_motion_min` threshold (env-var) to reduce false motion counts. For satellite-heavy lanes, consult [§9 battery estimates](#9-validation-and-testing). |
-| **Serial monitor shows `rtd.begin()` but no temperature reading** | MAX31865 SPI initialization failed; likely pin mismatch. | Verify SPI pinout: CS=D10, CLK=D13, SDO=D12, SDI=D11. Check 3.3V supply to MAX31865 VIN. Test SPI communication via a simple Arduino `SPI.read()` sketch. |
-| **`cargo_state.qo` transitions never fire, stays in "unknown"** | Motion data unavailable from `card.motion`. | Check firmware logs for `[cargo] card.motion failed`. Confirm `card.motion.mode` was applied (see warm-boot logs). If issue persists, Notecard may require power-cycle or firmware update. |
-
-
-## 11. Limitations and Next Steps
-
-The logger commits to a specific scope: independent, pallet-level evidence with a tamper-evident chain, scoped to the radios and physics that are actually available in a shipping container. Calibrated peak-G shock recording, cryptographic tamper proof, GPS, and lithium transport paperwork are real production concerns and are called out below; they extend this design rather than replacing it.
-
-### Remaining Design Boundaries
-
-The boundaries below are deliberate scope choices. Each marks where a production deployment will want to add a radio, a calibrated sensor, stronger cryptographic evidence, or compliance paperwork on top of the basic pallet-level logger.
-
-**Skylo satellite is opportunistic, not available inside enclosed metal structures.** Skylo uses geostationary (GEO) satellites; the antenna needs a clear view of the southern sky (northern hemisphere). Notes queue in flash and flush via cellular or WiFi when coverage resumes; enclosed-transit segments become **store-and-forward gaps, not data loss**. For routes where enclosed-transit exceeds the available battery reserve, a Starnote for Iridium — paired with a companion Notecard on a Notecarrier XI (a Starnote is a satellite add-on and does not operate standalone) — adds Iridium LEO coverage (global, even inside container terminals) as an alternative or supplement.
-
-**Shock detection is heuristic.** The `shock_detected` alert fires on accumulated motion-event count per interval, not on a calibrated peak-G threshold. For fragile biologics or fragile cargo requiring documented peak-G evidence, add a dedicated calibrated shock recorder alongside this device.
-
-**Alert cooldowns limit excursion record density.** The 30-minute cooldown suppresses repeat alerts during sustained excursions, which is good for on-call noise reduction. The `cargo_data.qo` hourly summaries and `cargo_log.qo` per-sample entries fill this gap — a complete excursion is visible in both the summary min/max fields and the per-sample `temp_c` values in the log. Compliance systems should use the log and summaries, **not just the alerts**, for excursion documentation.
-
-**Chain hash is integrity evidence within each boot segment, not cryptographic proof.** The `chain_crc` is a 32-bit non-cryptographic hash replayed within each `boot_seg`. It detects accidental corruption and provides evidence against casual manipulation of the log contents within a segment. A cold boot (planned or uncontrolled) starts a new `boot_seg`, resetting `seq` and `chain_crc` to 0; the remote log shows a new `boot_seg` value at the boundary, providing a traceable record of the reset event. Uncontrolled cold boots (power loss, brown-out) before `NotePayloadSaveAndSleep` completes lose the in-progress accumulator state and create a visible `boot_seg` increment; summary accumulators and alert cooldown timestamps from the interrupted cycle are not recoverable. The tilt baseline orientation **is** preserved across uncontrolled cold boots via `chain_boot.dbx`, so tilt detection does not silently re-seed from the post-reboot orientation. For deployments requiring strong cryptographic tamper evidence across boot boundaries, augment the chain approach with a server-side keyed MAC or timestamping service.
-
-**PT100 calibration document must be sourced at procurement.** The MAX31865 hardware is capable of NIST-traceable accuracy, but traceability is only realized if the PT100 probe is accompanied by a calibration certificate from an accredited laboratory. The specified Omega PR-21C-3-100-A-1/8-0600-M12-2 must be ordered with the **CAL-3** NIST-traceable calibration option (performed per ISO 10012-1 / ANSI/NCSL Z540-1). Verify calibration documentation at the time of procurement — a probe shipped without it does not satisfy the traceability requirement regardless of accuracy class.
-
-**No GPS location.** Notecard for Skylo includes GNSS for location, but this firmware does not request GPS fixes. Adding `card.location.mode` and including position in the summary and state-change Notes is a natural extension for multi-modal shipments where knowing *where* a temperature excursion or handling event occurred matters as much as *when*.
-
-**Lithium battery transport compliance.** The 3.7 V / 2 000 mAh Li-Po cell (7.4 Wh) is subject to transport safety regulations on every mode of carriage — UN 38.3 testing, IATA Section II (Packing Instruction 966/967) for air, IMDG Code Class 9 for ocean. Confirm compliance and sourcing of UN 38.3 documentation with your freight forwarder before the first shipment. For volume deployment, source cells from a manufacturer that supplies UN 38.3 test documentation on request.
-
-### Production Next Steps
-
-Once the basic logger is proven on real shipments, the following extensions are the natural progression toward a production cold-chain evidence system.
-
-**GPS context on every record** integrates GPS fixes into the summary and state-change Notes (`card.location.mode` + `card.location`) so excursion and handling records include geographic context.
-
-**Over-the-air firmware updates** wire [Notecard Outboard DFU](https://dev.blues.io/notehub/host-firmware-updates/notecard-outboard-firmware-update/) for remote firmware updates.
-
-**Mean Kinetic Temperature calculation** implements **MKT** in firmware or as a Notehub JSONata transform — MKT is the single-value thermal summary most pharma quality systems require for release decisions after transit.
-
-**Boot-segment cross-referencing** adds the `boot_seg` counter to `cargo_data.qo` summaries (currently present only in `cargo_log.qo`) so compliance systems can cross-reference summary records with the log's boot-segment boundaries without querying the raw log.
-
-**Cryptographic tamper evidence** evaluates adding a server-side keyed MAC or timestamping service on top of the `chain_crc` for deployments requiring strong tamper evidence.
-
-
-## 12. Summary
-
-The pharma quality manager (or food shipper's compliance lead) finally has the answer to "what actually happened to this pallet?": independent of the carrier, tied to the BOL, and accurate enough to drive a real acceptance or rejection decision. A NIST-traceable PT100 probe gives ±0.15 °C readings; a tamper-evident chain in `cargo_log.qo` lets a downstream verifier replay every five-minute sample within each boot segment and confirm that nothing was inserted, deleted, or modified in transit. Notecard for Skylo picks cellular, WiFi, or NTN satellite at every leg of the journey, queues through coverage gaps, and ships the original on-pallet timestamps when the radio comes back. Threshold values, sample cadence, summary cadence, and state-model parameters all retune from Notehub without a reflash, so the same logger generalizes from biologics on an ocean leg to fresh produce on a cross-country truck.
+**[Read the full documentation on dev.blues.io →](https://dev.blues.io/example-apps/accelerators/pallet-attached-cold-chain-logger/)**
diff --git a/89-pallet-attached-cold-chain-logger/banner.png b/89-pallet-attached-cold-chain-logger/banner.png
deleted file mode 100644
index 7045abe0..00000000
Binary files a/89-pallet-attached-cold-chain-logger/banner.png and /dev/null differ
diff --git a/89-pallet-attached-cold-chain-logger/diagrams/01-system-architecture.svg b/89-pallet-attached-cold-chain-logger/diagrams/01-system-architecture.svg
deleted file mode 100644
index 4664a46a..00000000
--- a/89-pallet-attached-cold-chain-logger/diagrams/01-system-architecture.svg
+++ /dev/null
@@ -1,62 +0,0 @@
-
diff --git a/89-pallet-attached-cold-chain-logger/diagrams/02-wiring-assembly.svg b/89-pallet-attached-cold-chain-logger/diagrams/02-wiring-assembly.svg
deleted file mode 100644
index d7f5b79e..00000000
--- a/89-pallet-attached-cold-chain-logger/diagrams/02-wiring-assembly.svg
+++ /dev/null
@@ -1,86 +0,0 @@
-
diff --git a/89-pallet-attached-cold-chain-logger/diagrams/03-data-flow.svg b/89-pallet-attached-cold-chain-logger/diagrams/03-data-flow.svg
deleted file mode 100644
index 13e3af1f..00000000
--- a/89-pallet-attached-cold-chain-logger/diagrams/03-data-flow.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-