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 786cb6c6..b0f4760d 100644
--- a/68-cnc-machine-spindle-load-cycle-time-tracker/README.md
+++ b/68-cnc-machine-spindle-load-cycle-time-tracker/README.md
@@ -8,35 +8,35 @@ This reference application is intended to provide inspiration and help you get s
-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 — 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**.
+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?
+**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.
+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.
+**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.
+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.
+**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.
+**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.
+**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)).
+**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.
+**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
@@ -46,7 +46,7 @@ Cellular is the answer. A [Blues Wireless for OPTA](https://shop.blues.com/produ
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.
+**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:
@@ -88,19 +88,19 @@ The Blues hardware ships with an active SIM including 500 MB of data and 10 year
-**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.
+**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.
+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.
+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).
+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.
+ > **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.
+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
@@ -108,9 +108,9 @@ The Blues hardware ships with an active SIM including 500 MB of data and 10 year
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.
+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.
+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 |
|---|---|---|
@@ -124,7 +124,7 @@ The Blues hardware ships with an active SIM including 500 MB of data and 10 year
> **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).
+ > **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.
@@ -135,7 +135,7 @@ The Blues hardware ships with an active SIM including 500 MB of data and 10 year
## 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:
+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 |
|---|---|
@@ -161,13 +161,13 @@ The firmware lives in the `firmware/` directory and is split into three files
### 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)).
+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× — meaningful for a device that may run for a decade against its included 500 MB.
+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):
@@ -189,7 +189,7 @@ Three [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-b
}
```
-`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.
+`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.
@@ -210,7 +210,7 @@ Three [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-b
}
```
-`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).
+`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):
@@ -225,21 +225,21 @@ Three [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-b
}
```
-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.
+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.
+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.
+- 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
+### Key code snippet 1: Notecard periodic sync configuration
```cpp
// Block until hub.set succeeds — Notecard configuration is mandatory.
@@ -260,7 +260,7 @@ while (true) {
}
```
-### Key code snippet 2 — Modbus TCP batch read (6 registers, 1 transaction)
+### 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.
@@ -281,7 +281,7 @@ 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
+### Key code snippet 3: Immediate alarm Note with sync:true
```cpp
// sync:true bypasses the hourly outbound window — the Notecard wakes the
@@ -336,21 +336,21 @@ arduino-cli monitor -p /dev/cu.usbmodem1234567 -c baudrate=115200

-**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.
+**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.
+- `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.
+- `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
@@ -385,7 +385,7 @@ Confirm: (a) idle current between syncs is in the µA range (ensures the device
-**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/).
+**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/).
@@ -404,29 +404,29 @@ Confirm: (a) idle current between syncs is in the µA range (ensures the device
## 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.
+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.
+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.
+**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.
+**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.
+**`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.
+**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.
+**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 minutes 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`.
+ `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.
+**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.
@@ -440,7 +440,7 @@ Each of the simplifications below is a deliberate scope choice — a place where
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, Haas NGC — each with correct addressing, scaling, and protocol translation where Modbus TCP is not natively available.
+**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.
@@ -456,4 +456,4 @@ Taking this monitor toward a production fleet means validating vendor-specific r
## 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.
+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.
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 64bfd7ce..23b109a6 100644
--- a/69-injection-molding-shot-to-shot-process-monitor/README.md
+++ b/69-injection-molding-shot-to-shot-process-monitor/README.md
@@ -8,7 +8,7 @@ This reference application is intended to provide inspiration and help you get s
-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).
+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
@@ -17,28 +17,28 @@ After completing this project, you will have:
- 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
+- 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.
+**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.
+**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.
+**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.
+**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.
+**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.
@@ -78,7 +78,7 @@ After completing this project, you will have:
-**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.
+**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.
@@ -124,17 +124,17 @@ All Blues parts ship with an active SIM including 500 MB of data and 10 years of

-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.
+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.
+**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.
+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. **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.
@@ -143,7 +143,7 @@ Inside the DIN-rail enclosure everything funnels back to the [Notecarrier CX](ht
- 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.
+- 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):**
@@ -155,16 +155,16 @@ Inside the DIN-rail enclosure everything funnels back to the [Notecarrier CX](ht
> **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.
+- 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.
+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.
+- Do not use the included flexible antenna inside the cabinet; retain it as a bench-test spare.
@@ -177,7 +177,7 @@ The Notecarrier CX includes a short flexible LTE antenna connected to the Noteca
- 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.
+- 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
@@ -187,7 +187,7 @@ The Notecarrier CX includes a short flexible LTE antenna connected to the Noteca
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.
+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 |
|---|---|---|
@@ -206,7 +206,7 @@ The Notecarrier CX includes a short flexible LTE antenna connected to the Noteca
## 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.
+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.
@@ -230,17 +230,17 @@ The firmware is a single Arduino sketch — [`firmware/injection_molding_shot_mo
**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.
+**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; 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.
+**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.
+**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.
+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.
+`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.
@@ -285,33 +285,33 @@ Sample `shot_alert.qo` Note body (immediate sync):
### 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 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.
+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.
+**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.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 — 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` — a transient I2C or Notecard failure therefore 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.
+**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.
+**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
+### 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.
+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");
@@ -334,7 +334,7 @@ if (!rsp || notecard.responseError(rsp)) {
if (rsp) notecard.deleteResponse(rsp);
```
-### Key code snippet 2 — immediate-sync alert
+### 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.
@@ -349,7 +349,7 @@ JAddNumberToObject(body, "peak_psi", peak_psi);
notecard.sendRequest(req);
```
-### Key code snippet 3 — feature extraction: mold-temperature trend slope (cool_c_s)
+### 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.
@@ -377,8 +377,8 @@ float denom = (float)(n * sxx - sx * sx);
**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.
+- `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.
@@ -394,19 +394,19 @@ float denom = (float)(n * sxx - sx * sx);
## 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.
+**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.
+**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.
+**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:
+**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.
+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.
+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.
@@ -427,7 +427,7 @@ Whole-system figures (Notecard + Notecarrier regulators + Cygnet host at the 5 V
| 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.
+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
@@ -444,17 +444,17 @@ The Mojo reports cumulative mAh to the Notecard at 1% accuracy over the Qwiic bu
## 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.
+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.
+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.
+**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.
+**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.
@@ -472,7 +472,7 @@ Each of the simplifications below is a deliberate scope choice — a place where
### 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.
+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.
@@ -484,7 +484,7 @@ Once a process engineer is running the basic shot monitor, the following extensi
**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.
+**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
diff --git a/70-trailer-manufacturer-connected-trailer-platform/README.md b/70-trailer-manufacturer-connected-trailer-platform/README.md
index 680dbf2a..a282bbd6 100644
--- a/70-trailer-manufacturer-connected-trailer-platform/README.md
+++ b/70-trailer-manufacturer-connected-trailer-platform/README.md
@@ -8,23 +8,23 @@ This reference application is intended to provide inspiration and help you get s
-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 summarises all five paths.
+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.
+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.
+**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.
+**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**
+**Platform sensor paths: implementation status**
**Fully implemented and buildable today:**
@@ -36,9 +36,9 @@ A trailer OEM that wants to own the connectivity layer — rather than ceding it
- 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:**
+**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).
+- **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).
@@ -47,17 +47,17 @@ A trailer OEM that wants to own the connectivity layer — rather than ceding it

-**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 serialised `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.
+**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.
+**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.
+**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.
+**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.
+**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.
@@ -139,7 +139,7 @@ Here is a sample Note this device emits:
| 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 maximise impedance in the surge path. |
+| 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). |
@@ -167,18 +167,18 @@ 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.
+- **+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`).
+- **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.
+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.
+**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)
@@ -186,17 +186,17 @@ Mount the enclosure on the nose wall at a height that keeps all connectors above
**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.
+**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.
+- **+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.
+- **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):**
@@ -218,7 +218,7 @@ When a J2497 custom board is built, its connections to the Notecarrier CX are:
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.
+- **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).
@@ -261,7 +261,7 @@ The firmware spans three files, splitting orchestration, shared types, and senso
| 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()` serialises state and sleeps via `NotePayloadSaveAndSleep()`. Also contains Notecard configuration, trailer state machine, alert evaluation, and summary-Note assembly. |
+| [`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()`. |
@@ -274,7 +274,7 @@ Dependencies:
| Responsibility | Function |
|---|---|
-| State serialisation to Notecard + host power-off via ATTN | `NotePayloadSaveAndSleep()` (Blues library) in `loop()` |
+| 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()` |
@@ -291,20 +291,20 @@ Dependencies:
### 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).
+- **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.
+- **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.
+**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.
+**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):
+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,
@@ -334,7 +334,7 @@ Example `trailer_summary.qo` body — bench build with real cargo-air thermistor
*`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):
+**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",
@@ -351,9 +351,9 @@ Example `trailer_summary.qo` body — bench build with real cargo-air thermistor
```
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.
+**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):
+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
@@ -381,7 +381,7 @@ Example `trailer_summary.qo` body — full platform, frozen-food transit (12-sam
}
```
-Example `trailer_alert.qo` body (with `sync:true`, immediately transmitted) — `door_open_transit` from a real door event:
+Example `trailer_alert.qo` body (with `sync:true`, immediately transmitted), `door_open_transit` from a real door event:
```json
{
"alert": "door_open_transit",
@@ -399,7 +399,7 @@ Example `trailer_alert.qo` body (with `sync:true`, immediately transmitted) —
### Low-power strategy
-**Dwell-capable host sleep via `card.attn`.** After each sample cycle `loop()` serialises `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.
+**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:
@@ -412,7 +412,7 @@ The key power budget figures:
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-initialises `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.
+**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.
@@ -426,13 +426,13 @@ Operators can extend `sample_interval_sec` via environment variable during long-
- 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.
+- 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()` serialises `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.
+`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:
@@ -461,7 +461,7 @@ void setup() {
}
if (!restored) {
memset(&g_ps, 0, sizeof(g_ps));
- // ... initialise fields, call notecardInit() ...
+ // ... initialize fields, call notecardInit() ...
}
fetchEnvOverrides(); // always re-fetch OTA config changes
@@ -542,7 +542,7 @@ static void sendAlert(uint8_t idx, uint32_t nowEpoch) {
### 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.
+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() {
@@ -587,7 +587,7 @@ static bool defineTemplate() {
### 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.
+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) {
@@ -629,15 +629,15 @@ static void updateTrailerState(uint32_t nowEpoch) {
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)
+- 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.
+- `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.
@@ -645,19 +645,19 @@ The Notecard's `card.attn` wakes the host every `sample_interval_sec` (default 3
**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.
+*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.
+*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.
+**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.**
@@ -669,7 +669,7 @@ The Notecard's `card.attn` wakes the host every `sample_interval_sec` (default 3
|---|---|---|---|
| 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.
+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 |
|---|---|---|
@@ -682,21 +682,21 @@ The Notecard idle figure is the only Blues-published power specification for the
**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.
+**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, 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.
+**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.
+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.
+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.
@@ -706,7 +706,7 @@ The items below are the honest accounting of what's still in front of any OEM th
| **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.
+ 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.
@@ -714,7 +714,7 @@ The items below are the honest accounting of what's still in front of any OEM th
**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.**
+**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.
@@ -730,7 +730,7 @@ Once the three day-one sensor paths are running against real hardware, the follo
**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).
+**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.
@@ -744,4 +744,4 @@ Once the three day-one sensor paths are running against real hardware, the follo
## 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.
+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.
diff --git a/71-legacy-diesel-generator-fleet-performance-uplift/README.md b/71-legacy-diesel-generator-fleet-performance-uplift/README.md
index 075ffe1c..c1fa273f 100644
--- a/71-legacy-diesel-generator-fleet-performance-uplift/README.md
+++ b/71-legacy-diesel-generator-fleet-performance-uplift/README.md
@@ -8,21 +8,21 @@ This reference application is intended to provide inspiration and help you get s
-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 — no writes to start/stop or safety circuits.
+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 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*.
+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.
+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.
+**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.
+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.
@@ -30,13 +30,13 @@ This is the key architectural insight: a well-designed generator panel already h

-**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.
+**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).
+**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.
+**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
@@ -86,7 +86,7 @@ This is the key architectural insight: a well-designed generator panel already h
}
```
-**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).
+**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
@@ -112,7 +112,7 @@ The Blues hardware ships with an active SIM including 500 MB of data and 10 year
-**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.
+**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.
@@ -120,13 +120,13 @@ The Blues hardware ships with an active SIM including 500 MB of data and 10 year
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.
+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−`.
+ - 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.
@@ -142,7 +142,7 @@ See [§5](#5-wiring-and-assembly) for detailed wiring instructions.
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.
+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 |
|---|---|---|
@@ -171,7 +171,7 @@ See [§5](#5-wiring-and-assembly) for detailed wiring instructions.
## 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.
+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).
@@ -196,7 +196,7 @@ The firmware is a single Arduino sketch — [`firmware/diesel_gen_monitor/diesel
### 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.
+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.
@@ -204,7 +204,7 @@ All seven must succeed in a single attempt before the sample is marked valid. Pa
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) — example 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:
+`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
{
@@ -232,13 +232,13 @@ Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-ban
**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.
+- `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:
+`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`:
@@ -297,26 +297,26 @@ Two [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-ban
**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.
+- `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.
+- `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.
+**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.
+- 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.
@@ -324,7 +324,7 @@ The OPTA + expansion draws continuously from the generator panel's battery-backe
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.
+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.
@@ -379,7 +379,7 @@ if (g_alarm_mask_fts != 0) {
`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).
+- `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.
@@ -400,7 +400,7 @@ 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.
+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) {
@@ -427,23 +427,23 @@ if (stats.run_samples > 0) {
**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.
+- `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.
+**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.
+*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`.
+*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
@@ -451,7 +451,7 @@ if (stats.run_samples > 0) {
**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.
+**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/):
@@ -465,7 +465,7 @@ The OPTA host runs continuously — the firmware's millis-based scheduler keeps
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.
+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
@@ -473,7 +473,7 @@ In the field, the generator's start battery sustains the control bus through a m
- 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.
+- 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`**
@@ -496,27 +496,27 @@ This reference design is deliberately scoped to the path that takes a facility m
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.**
+**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.
+**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.
+**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.
+**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.
+**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.
+**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.
@@ -530,4 +530,4 @@ Once a real fleet is reporting in, the following extensions are the natural prog
## 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.
+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.
diff --git a/72-solar-array-string-level-performance-dashboard/README.md b/72-solar-array-string-level-performance-dashboard/README.md
index 8e36beb6..0b2ce4eb 100644
--- a/72-solar-array-string-level-performance-dashboard/README.md
+++ b/72-solar-array-string-level-performance-dashboard/README.md
@@ -8,17 +8,17 @@ This reference application is intended to provide inspiration and help you get s
-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 — catching soiling, shading, and bad modules from wherever the array happens to be, without WiFi, without site IT involvement, and without touching the existing inverter.
+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.
+**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.
+**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.
@@ -26,7 +26,7 @@ This project adds that missing network path. It reads per-string DC voltage and
## 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.
+**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.
@@ -36,13 +36,13 @@ This project adds that missing network path. It reads per-string DC voltage and

-**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.
+**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 (monocrystalline vs bifacial) — each carrying its own environment-variable overrides without ever forking the 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.
+**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
@@ -54,7 +54,7 @@ This project adds that missing network path. It reads per-string DC voltage and
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.
+6. Check Notehub. You should see `solar_summary.qo` and/or `solar_alert.qo` events appear within the next sample interval.
**Build Instructions:**
@@ -110,7 +110,7 @@ Here is a sample Note this device emits:
| 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.
+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
@@ -128,7 +128,7 @@ Do not work on wiring connected to the PV array, combiner, or AC supply without
-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.
+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):**
@@ -174,7 +174,7 @@ No external jumper is required. The Notecarrier CX gates the Cygnet host's 3.3 V
**Mojo data connection (bench validation only):**
-- The power path above places Mojo inline on the 5V rail — no additional power wiring is required.
+- 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.
@@ -183,8 +183,8 @@ No external jumper is required. The Notecarrier CX gates the Cygnet host's 3.3 V
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.
+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 |
|---|---|---|
@@ -248,7 +248,7 @@ The firmware is split between a main sketch — [`firmware/solar_string_monitor/
### 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× — meaningful at 24 summary Notes/day per device over a multi-year deployment on a shared SIM.
+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):
@@ -307,19 +307,19 @@ Field reference:
### 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 serialises `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.
+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.
+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.
+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` — no device is silently left unconfigured.
+- 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()` — a typo in the Notehub UI can't drive `g_perf_threshold` to a value that fires every sample or never fires at all.
+- 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
@@ -344,7 +344,7 @@ if (n >= 2) {
### 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.
+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);
@@ -394,7 +394,7 @@ NotePayloadSaveAndSleep(&payload, g_sample_interval_sec, NULL);
-**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 behaviour; see [§11 Limitations](#11-limitations-and-next-steps).
+**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).
@@ -407,7 +407,7 @@ NotePayloadSaveAndSleep(&payload, g_sample_interval_sec, NULL);
## 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.
+**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.
@@ -415,21 +415,21 @@ NotePayloadSaveAndSleep(&payload, g_sample_interval_sec, NULL);
-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`.
+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 — they will not equal the bare-Notecard datasheet values. Use the Mojo to characterise the actual idle and burst figures for your specific stack.
+**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 characterise 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 characterised per-session energy figures | Notecard-dominated; Cygnet is off during the sync window |
+| 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 characterised 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.
+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.
@@ -450,19 +450,20 @@ With the default cadence (5-minute samples, hourly sync), a healthy trace shows
(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.
+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 serialised 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.**
+**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.**
@@ -476,7 +477,7 @@ Each item below is a place where the reference build keeps the signal model or t
**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.
+**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
@@ -496,4 +497,4 @@ Once the basic dashboard is live, the following extensions are the natural progr
## 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 — sending 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, replacing the quarterly look-back with continuous visibility into the 3–8% of annual production that used to leak away silently.
+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.
diff --git a/74-returnable-container-tote-pool-tracker/README.md b/74-returnable-container-tote-pool-tracker/README.md
index 697f18d3..c09863db 100644
--- a/74-returnable-container-tote-pool-tracker/README.md
+++ b/74-returnable-container-tote-pool-tracker/README.md
@@ -12,13 +12,13 @@ This project is an [asset location tracking](https://blues.com/solutions-locatio
## 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 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.
+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.
+**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.
@@ -26,7 +26,7 @@ This project takes a better approach. The Notecard's built-in accelerometer watc
-**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 vapour may be present — unless the complete assembly (enclosure, battery, antenna, and all electronics) has been certified for that environment by a recognised testing body.
+**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.
@@ -74,15 +74,15 @@ The firmware emits two Note types; Notehub automatically appends location and ro
}
```
-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 required to produce location data.
+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.
+**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 — 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.
+**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.
+**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.
+**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
@@ -90,7 +90,7 @@ The `reason` field is `0` (boot), `1` (heartbeat), or `2` (low battery). The `wh
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.
+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.
@@ -136,7 +136,7 @@ Here is a sample Note this device emits:
| [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.
+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
@@ -144,7 +144,7 @@ Blues hardware ships with an active SIM including 500 MB of data and 10 years of
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.
+**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.
@@ -152,7 +152,7 @@ One of the quiet advantages of this design is how little wiring there is to talk
*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.
+**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.
@@ -182,11 +182,11 @@ One of the quiet advantages of this design is how little wiring there is to talk
**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.
+**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.
+**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 |
|---|---|---|
@@ -207,7 +207,7 @@ The Notecard initiates an early session on first boot, so events typically appea
## 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:
+The firmware spans three files in the `firmware/` directory; keep all three together, since the helpers are referenced from the main sketch:
| File | Role |
|------|------|
@@ -224,7 +224,7 @@ The Arduino toolchain automatically compiles the `.h` and `.cpp` alongside the `
- **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 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`:**
@@ -260,9 +260,9 @@ After upload, serial output is **disabled by default** — release builds intent
### 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"`.
+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 — 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)).
+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
@@ -281,7 +281,7 @@ Location is provided by [`card.triangulate`](https://dev.blues.io/notecard/notec
}
```
-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.
+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)):
@@ -298,7 +298,7 @@ Motion events are untemplated — they're low-volume (at most a handful per day
}
```
-`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.
+`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
@@ -316,7 +316,7 @@ Both Note types are queued with `sync:true`, which causes the Notecard to open a
- `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.
+- 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
@@ -409,7 +409,7 @@ notecard.sendRequest(req);
**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).
+**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
@@ -470,7 +470,7 @@ Useful Mojo bench validation: leave the assembly running for 24 h and confirm th
## 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.
+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
@@ -478,7 +478,7 @@ Each item below is a deliberate scope choice that keeps the prototype path short
**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 vapours 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.**
+**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.
@@ -488,7 +488,7 @@ Each item below is a deliberate scope choice that keeps the prototype path short
**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.
+**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.
@@ -508,4 +508,4 @@ Taking this prototype toward a multi-year deployed fleet means re-engineering th
## 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.
+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.
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 7bdc5726..23bfb8b0 100644
--- a/75-heavy-equipment-hours-of-use-utilization-tracker/README.md
+++ b/75-heavy-equipment-hours-of-use-utilization-tracker/README.md
@@ -8,15 +8,15 @@ This reference application is intended to provide inspiration and help you get s
-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).
+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.
+**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.
+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.
+**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.
@@ -26,13 +26,13 @@ Vibration-based hour detection solves the retrofit problem entirely. A small enc

-**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.
+**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 required after the fence parameters are applied.
+**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.
+**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.
+**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
@@ -74,13 +74,13 @@ Here is a sample Note this device emits:
| 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.
+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.
+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:
@@ -94,14 +94,14 @@ Pin-by-pin:
**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.
+- **`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.
+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.
+**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.
+**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:
@@ -116,11 +116,11 @@ The MAIN antenna is Skylo-certified as a unit with the NOTE-NBGLWX — do not su
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 — 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 — Skylo availability depends on sky visibility and signal conditions, not orbital pass timing.
+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.
+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 |
|---|---|---|
@@ -246,13 +246,13 @@ Each wake, the host collects **208 accelerometer samples at 104 Hz** (approximat
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.
+- **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.
+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
@@ -281,7 +281,7 @@ Both Notefiles use [compact templates](https://dev.blues.io/notecard/notecard-wa
}
```
-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.
+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
@@ -289,13 +289,13 @@ Power efficiency matters for a solar-trickle-charged deployment. Three levers ar
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.
+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.
+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.
+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
@@ -405,7 +405,7 @@ notecard.sendRequest(notecard.newRequest("hub.sync"));
**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:
+**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`.
@@ -442,9 +442,9 @@ A healthy Mojo trace at default settings will show the following pattern:
- **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.
+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.
+**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
@@ -463,7 +463,7 @@ If the baseline is continuously 10+ mA, the Cygnet is not sleeping — confirm t
## 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.
+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
@@ -473,15 +473,15 @@ Each of the following is a deliberate shortcut that keeps the reference build re
**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 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.
+**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.
+**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.
+**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.
@@ -503,4 +503,4 @@ These are the natural extensions for hardening the tracker across a real fleet,
## 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.
+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.
diff --git a/76-untethered-trailer-chassis-fleet-tracker/README.md b/76-untethered-trailer-chassis-fleet-tracker/README.md
index 8a58198f..03fb6b89 100644
--- a/76-untethered-trailer-chassis-fleet-tracker/README.md
+++ b/76-untethered-trailer-chassis-fleet-tracker/README.md
@@ -8,15 +8,15 @@ This reference application is intended to provide inspiration and help you get s
-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).
+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.
+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.
+**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.
+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:
@@ -24,11 +24,11 @@ This matters because trailers and chassis represent enormous capital. A $100,000
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.
+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.
+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.
+**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
@@ -37,9 +37,9 @@ Third, **power is scarce**. Trailers have no reliable 12V auxiliary hookup — t
### 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.
+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.
+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
@@ -105,7 +105,7 @@ Here is a sample Note this device emits:
**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.
+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
@@ -170,7 +170,7 @@ The Swan communicates with the cellular Notecard over the Notecarrier XI's onboa
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.
+**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.
@@ -185,7 +185,7 @@ An intermodal chassis has no roof — it is a flat, low steel frame sized for a
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.
+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
@@ -292,11 +292,11 @@ arduino-cli upload -b STMicroelectronics:stm32:GenU5:pnum=SWAN_R5 \
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.
+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.
+**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.
@@ -307,7 +307,7 @@ For **arrival events**, `captureGnssState()` is called while GPS is still in per
### 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.
+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):**
@@ -350,13 +350,13 @@ Sample `trailer_heartbeat.qo` (parked, battery healthy, GPS fix available):
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.
+`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.
+**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.
@@ -364,7 +364,7 @@ A downward trend in `volt` across consecutive heartbeats is an early warning tha
- 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.
+- `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.
@@ -554,19 +554,19 @@ For the arrival event, Note the **asymmetric detection latency**: once the track
## 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.
+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.
+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.
+**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).
+**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.
+**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.
@@ -574,11 +574,11 @@ The following are the deliberate trade-offs in this build — each is something
**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.
+**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.
+**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
@@ -595,4 +595,4 @@ Beyond the location-and-dwell core, these extensions integrate the tracker with
## 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?"
+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?"
diff --git a/78-rail-car-condition-interchange-tracker/README.md b/78-rail-car-condition-interchange-tracker/README.md
index d99b3a2d..0f6e445c 100644
--- a/78-rail-car-condition-interchange-tracker/README.md
+++ b/78-rail-car-condition-interchange-tracker/README.md
@@ -8,39 +8,39 @@ This reference application is intended to provide inspiration and help you get s
-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.
+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.
+**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.
+**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.
+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.
+**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.
+**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.
+**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.
+**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.
+**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):**
@@ -253,7 +253,7 @@ The firmware compiles into two distinct profiles controlled by a single `#define
| **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.
+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.
@@ -304,7 +304,7 @@ arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET -p /dev/cu.usbm
[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).
+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
@@ -324,16 +324,16 @@ After the first sample cycle, the host powers off until the next ATTN fire (defa
### 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.
+- **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.
+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.
+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):
@@ -364,7 +364,7 @@ Alert types and their `value` fields:
### 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.
+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:
@@ -394,7 +394,7 @@ At high charge (good solar harvest), the Notecard syncs every 2 hours; at normal
### 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.
+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");
@@ -433,7 +433,7 @@ 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.
+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;
@@ -453,7 +453,7 @@ return (validSamples > 0) ? peakG : NAN;
`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-initialised 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.
+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
@@ -480,7 +480,7 @@ if (!sleepOk) { /* fallback card.attn sleep */ }
**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_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.
@@ -548,15 +548,15 @@ Mojo is a bench-validation and per-firmware-revision regression tool. Field unit
## 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.
+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.
+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 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`.
+**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.
@@ -590,4 +590,4 @@ These extensions take the tracker from a readable reference build toward fleet-g
## 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.
+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.
diff --git a/79-utility-distribution-transformer-load-monitor/README.md b/79-utility-distribution-transformer-load-monitor/README.md
index 3f2c1cb5..ea274b9d 100644
--- a/79-utility-distribution-transformer-load-monitor/README.md
+++ b/79-utility-distribution-transformer-load-monitor/README.md
@@ -8,25 +8,25 @@ This reference application is intended to provide inspiration and help you get s
-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.
+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.
+**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.
+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.
+**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.
+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.
+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.
@@ -39,13 +39,13 @@ CT installation is **non-invasive**: the split-core sensors clamp directly onto

-**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.
+**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.)
+**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.
+**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.
+**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
@@ -128,7 +128,7 @@ All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notec
### 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 centred at Vcc/2 so it sits squarely within the Cygnet ADC's 0–3.3V input range.
+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)
@@ -150,7 +150,7 @@ Each CT channel requires a small passive network that converts the CT's AC curre
**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 centres the AC signal within the ADC's single-supply input range.
+- **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.
@@ -172,7 +172,7 @@ Connect the cellular antenna to the Notecard Cell+WiFi (MBGLW) u.FL port on the
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 aluminium 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.
+- **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.
@@ -312,7 +312,7 @@ The firmware defines several key tuning constants in `transformer_load_monitor_h
### 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 centred 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; 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.
+**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.
@@ -340,7 +340,7 @@ The `extra` field carries alert-type-specific context: for `overload` it is the
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.
+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.
@@ -501,13 +501,13 @@ Mojo is a bench-validation and regression tool — it is not required in product
## 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 — 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.
+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.
+**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.
@@ -515,13 +515,13 @@ Each of these is a deliberate scope boundary that keeps the alerting and trendin
**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.
+**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.
+**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.
+**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.
@@ -550,4 +550,4 @@ These additions take the monitor from a controlled trial toward a permanently en
## 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.
+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.
diff --git a/80-ev-charger-session-utilization-monitor/README.md b/80-ev-charger-session-utilization-monitor/README.md
index f8fcd8eb..78a0c222 100644
--- a/80-ev-charger-session-utilization-monitor/README.md
+++ b/80-ev-charger-session-utilization-monitor/README.md
@@ -8,7 +8,7 @@ This reference application is intended to provide inspiration and help you get s
-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.
+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
@@ -17,15 +17,15 @@ This project is a cellular [energy monitoring](https://blues.com/solutions-energ
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.
+**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.
+**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.
+**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.
@@ -34,11 +34,11 @@ Cellular sidesteps all of that. A Notecard Cell+WiFi (MBGLW) with its included p

-**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 penalises 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.
+**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 organisation layer — one fleet per property keeps environment-variable overrides (a site's known line voltage, say) scoped correctly.
+**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.
@@ -184,7 +184,7 @@ Three event types matter:
"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 — 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-energised 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.
+ `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
{
@@ -195,8 +195,8 @@ Three event types matter:
"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:
+ `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",
@@ -247,7 +247,7 @@ Open the serial monitor at **115200 baud** after flashing. On cold boot you'll s
| Responsibility | Function |
|---|---|
| Notecard configuration (`hub.set`, template registration, accelerometer disable) | `initNotecard`, `defineTemplates` |
-| RS-485 / Modbus initialisation (every wake) | `initModbus` |
+| 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` |
@@ -259,7 +259,7 @@ Open the serial monitor at **115200 baud** after flashing. On cold boot you'll s
### 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.
+`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.
@@ -293,9 +293,9 @@ notecard.sendRequest(req);
### 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 serialises 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.
+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.
+`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
@@ -333,7 +333,7 @@ bool emitSessionNote(float kwh, float peak_w,
### 7.8 Key code snippet: state persistence across sleep
-`NotePayloadSaveAndSleep` serialises 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.
+`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};
@@ -387,7 +387,7 @@ window_total_kwh = import_kwh_now − import_kwh_at_window_open
**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 energised. This is the record that answers both "do we need more chargers?" and "was this charger actually available to be used?"
+- `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.
@@ -405,7 +405,7 @@ window_total_kwh = import_kwh_now − import_kwh_at_window_open
**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 initialisation, env-var fetch, and sleep cycle correctly.
+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/):
@@ -415,7 +415,7 @@ If you do not have a live SDM120 available for initial bring-up, you can verify
| 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 — characterise it on the bench with a known load pattern and use that measured figure as your deployment baseline.
+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).
@@ -426,7 +426,7 @@ Mojo is bench-validation tooling — deployed units running from a panel supply
## 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.
+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
@@ -436,7 +436,7 @@ Each boundary below is a place where the meter-based approach trades authoritati
**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 energised. 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 energised 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.
+**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.
@@ -450,7 +450,7 @@ Each boundary below is a place where the meter-based approach trades authoritati
### 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.
+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.
@@ -464,5 +464,5 @@ Once the basic meter monitor is running, these extensions move it toward billing
## 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.
+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.
diff --git a/81-commercial-tenant-sub-metering-bridge/README.md b/81-commercial-tenant-sub-metering-bridge/README.md
index a0687adc..9af1c9eb 100644
--- a/81-commercial-tenant-sub-metering-bridge/README.md
+++ b/81-commercial-tenant-sub-metering-bridge/README.md
@@ -12,13 +12,13 @@ This project is a cellular [energy monitoring](https://blues.com/solutions-energ
## 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.** 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.
+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.
+**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.
@@ -26,11 +26,11 @@ This project takes a different path. Instead of installing meters with network c
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
+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.
+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.
@@ -40,15 +40,15 @@ This is not a niche edge case. Billing disputes are among the most contentious i

-**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.
+**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/api-introduction/). 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.
+**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/api-introduction/). 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.
+**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
@@ -84,9 +84,9 @@ Use this BOM for firmware bring-up, calibration, and evaluation. The ZMPT101B mo
| [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-stabilisation) | 4 | One per integrator channel; limits DC gain of the Miller integrator to prevent output rail saturation from op-amp offset drift. |
+| 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 centres each AC signal at 1.65 V for the single-supply ADC. |
+| 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. |
@@ -330,23 +330,23 @@ After flashing, open the serial monitor at **115200 baud**. On first power-on yo
### 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.
+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.
+**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.
+**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.
+**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 — 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.
+**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`.
+**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
@@ -354,7 +354,7 @@ A Rogowski coil installed with reversed lead polarity produces a consistently ne
**`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.
+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
{
@@ -370,25 +370,25 @@ Nine 4-byte floats: estimated interval energy (Wh), 15-minute blocked-average de
}
```
-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/api-introduction/), 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.
+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/api-introduction/), 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 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.
+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.
+**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.
+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.
+**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.** Both `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:
+**`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);
@@ -397,12 +397,12 @@ 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.
+**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:
+**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.
+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.
@@ -440,7 +440,7 @@ bool defineTemplates(void) {
### 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.
+`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};
@@ -492,8 +492,6 @@ NotePayloadSaveAndSleep(&out, cfg.sample_interval_sec, NULL);
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

@@ -503,7 +501,7 @@ If compilation fails with a `PRODUCT_UID is not defined` warning, revisit step 2
**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.
+`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/api-introduction/) or a downstream aggregation pipeline. Notehub stores every Note durably; no device-side aggregate is needed to reconstruct any monthly total.
@@ -534,7 +532,7 @@ Skipping calibration leaves you with readings that may be off by ±10–30% depe
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.
+- **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
@@ -550,15 +548,15 @@ The dominant energy cost is the once-per-hour cellular session. Because the supp
| 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.
+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.
+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.
+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.
@@ -584,14 +582,14 @@ Taking the bridge toward a production rollout means calibrating each installed c
**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:
+**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.
+ - *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.
@@ -605,4 +603,4 @@ Taking the bridge toward a production rollout means calibrating each installed c
## 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.
+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.
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 81bfb468..b2143460 100644
--- a/82-aerial-lift-rental-equipment-battery-health-monitor/README.md
+++ b/82-aerial-lift-rental-equipment-battery-health-monitor/README.md
@@ -8,17 +8,17 @@ This reference application is intended to provide inspiration and help you get s
-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.
+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 — 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.
+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.
+**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.
@@ -28,11 +28,11 @@ This project is the watcher. A small monitoring device clipped to the pack reads

-**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.
+**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.
+**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.
+**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.
@@ -215,7 +215,7 @@ The Waveshare MCP2515 CAN Bus Module runs at 5V logic; all SPI lines must pass t
**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.
+> **⚠ 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)
@@ -296,8 +296,8 @@ Four files in [`firmware/lift_battery_monitor/`](firmware/lift_battery_monitor/)
**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`).
+- **`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.
@@ -383,9 +383,9 @@ All five fields are always present on every alert Note. `extra_v` is `0.0` for a
### 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 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.
+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
@@ -442,7 +442,7 @@ 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.
+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};
@@ -485,7 +485,7 @@ Every `sample_interval_s` seconds the firmware wakes, reads up to four data sour
**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.
+- 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:**
@@ -513,7 +513,7 @@ All alert Notes share a fixed compact schema with five fields always present: `a
## 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.
+**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.
@@ -553,15 +553,15 @@ Mojo is not deployed to the field unit; its role ends when a firmware revision p
## 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.
+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 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.
+**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.**
@@ -571,17 +571,17 @@ The simplifications below are scope choices — each names a boundary where a pr
**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.
+**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.
+**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.
+**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.
+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.
@@ -599,4 +599,4 @@ Once the basic monitor is on a machine, the following extensions harden it for a
## 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.
+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.
diff --git a/83-off-grid-solar-battery-site-controller/README.md b/83-off-grid-solar-battery-site-controller/README.md
index 1e24907a..bcaaf43e 100644
--- a/83-off-grid-solar-battery-site-controller/README.md
+++ b/83-off-grid-solar-battery-site-controller/README.md
@@ -24,8 +24,8 @@ This project is a bank-level solar and battery monitoring solution — a Blues [
-- **[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.
+- **[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.
@@ -35,11 +35,11 @@ This project is a bank-level solar and battery monitoring solution — a Blues [
**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.
+**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.
+**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.
+**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
@@ -62,7 +62,7 @@ To get a working monitor up and running in ~2 hours:
- 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).
+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:
@@ -101,17 +101,17 @@ Here is a sample Note this device emits:
| [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 minimise 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. |
+| 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.
+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.
+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.
@@ -145,7 +145,7 @@ The standard Victron VE.Direct cable (ASS030530200 / ASS030530300) terminates in
- **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.
+**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**.
@@ -166,7 +166,7 @@ Wire the complete power chain in this order:
-**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.
+**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.
@@ -174,18 +174,18 @@ Wire the complete power chain in this order:
**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.
+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, aluminium 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.
+- **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.
+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.
+- 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 minimise insertion loss, especially on the satellite band.
+- 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.
@@ -196,10 +196,10 @@ VE.Direct cables are short (0.9 m or 1.8 m). Mount the Notecarrier CX close to t
## 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`.
+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.
+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 |
@@ -217,7 +217,7 @@ VE.Direct cables are short (0.9 m or 1.8 m). Mount the Notecarrier CX close to t
### 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.
+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.
@@ -229,7 +229,7 @@ The NOTE-NBGLWX manages radio-mode selection internally — the firmware is iden
```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 kilometres. 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.
+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.
@@ -319,13 +319,13 @@ After upload, open the serial monitor at **115200 baud**. On first boot you'll s
| Sample accumulation into rolling window averages | `accumulate` |
| Alert evaluation and immediate-sync Notes | `checkAndSendAlerts`, `checkHarvestDeficit`, `sendAlert` |
| Summary Note construction and emission | `sendSummary` |
-| Persistent state serialisation across sleep cycles | `PersistState` struct + `NotePayloadSaveAndSleep` / `NotePayloadRetrieveAfterSleep` |
+| 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.
+`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.
@@ -337,9 +337,9 @@ After upload, open the serial monitor at **115200 baud**. On first boot you'll s
- `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.
+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:
+`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` |
|---|---|---|---|
@@ -350,11 +350,11 @@ This fixed-schema approach follows established Blues reference design practice:
### 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` serialises 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 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.
+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
@@ -458,7 +458,7 @@ Every 15 minutes (default `sample_interval_sec = 900`) the firmware wakes, reads
> **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.
+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).
@@ -491,7 +491,7 @@ Mojo is a **bench bring-up tool**, not a production sensor. Once a firmware revi
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 — 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.
+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
@@ -507,7 +507,7 @@ For the truly remote sites where terrestrial cellular coverage is marginal or ab
| 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.
+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
@@ -516,7 +516,7 @@ This reference design intentionally stays narrow: it answers the site-uptime que
### 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.
+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.
@@ -524,9 +524,9 @@ The simplifications below are deliberate scope choices — each marks where this
**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.
+**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 recognise its label set.
+**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.
diff --git a/84-remote-cabinet-backup-battery-sentinel/README.md b/84-remote-cabinet-backup-battery-sentinel/README.md
index 136d02d8..97f23275 100644
--- a/84-remote-cabinet-backup-battery-sentinel/README.md
+++ b/84-remote-cabinet-backup-battery-sentinel/README.md
@@ -18,7 +18,7 @@ The failure signal that *does* show up early is float current. A healthy VRLA ba
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, but float-current trending as a capacity-health proxy does not apply to LFP without chemistry-specific calibration. See §7 for per-alert chemistry guidance and §9 for LFP threshold commissioning Notes.
+**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.
@@ -34,17 +34,17 @@ The Notecard manages its own cellular session against the supported carrier netw
**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.
+**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.
+**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).
+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/
@@ -52,8 +52,8 @@ The Notecard manages its own cellular session against the supported carrier netw
```
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)
+ - `_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.
@@ -104,7 +104,7 @@ The Notecard manages its own cellular session against the supported carrier netw
| 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.
+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)
@@ -148,7 +148,7 @@ The 2000 mAh LiPo on the JST connector is charged from the regulated 5 V supply
**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.
+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 (+)
@@ -191,7 +191,7 @@ 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`.
+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.
@@ -212,9 +212,9 @@ Remove Mojo from the power path for production deployment; see §9.
| `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-initialises 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. |
+ | `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.
+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
@@ -240,9 +240,9 @@ The firmware is split across three files in `firmware/cabinet_battery_sentinel/`
**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.
+- **`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.
@@ -268,7 +268,7 @@ After upload, open the serial monitor at **115200 baud**. On each wake the firmw
[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.
+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
@@ -287,25 +287,25 @@ Then the host powers off for the sample interval and the monitor goes quiet. `IN
### 7.3 Sensor reading strategy
-**INA228 (voltage and current).** The INA228 loses power while the Cygnet is sleeping, so it's re-initialised 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`.
+**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.
+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.
+**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.
+`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.
+- `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.
@@ -313,17 +313,17 @@ 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 serialised into Notecard flash by `NotePayloadSaveAndSleep` before the host shuts down, and deserialised by `NotePayloadRetrieveAfterSleep` on the next wake, so the rolling window accumulates correctly across hundreds of sleep cycles per day.
+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.
+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 initialising. `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 desynchronise the recorded cadence from the Notecard's actual setting.
+- **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.
+- **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.
@@ -374,7 +374,7 @@ notecard.sendRequest(req);
### 7.9 Key code snippet 3 — sleep and state persistence
-`NotePayloadSaveAndSleep` serialises 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.
+`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};
@@ -416,9 +416,9 @@ NotePayloadSaveAndSleep(&payload, g_sampleSec, NULL);
## 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.
+**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.
+**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:
@@ -444,7 +444,7 @@ Use the Mojo to measure whole-device energy over a full 24-hour cycle on your sp
- **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.
+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.
diff --git a/85-cellular-medication-adherence-pillbox/README.md b/85-cellular-medication-adherence-pillbox/README.md
index ce850bec..38995877 100644
--- a/85-cellular-medication-adherence-pillbox/README.md
+++ b/85-cellular-medication-adherence-pillbox/README.md
@@ -8,7 +8,7 @@ This reference application is intended to provide inspiration and help you get s
-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.
+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
@@ -32,19 +32,19 @@ Existing IoT pillboxes have tried to close this gap, and most fail in the same p
**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.
+**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.
+**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).
+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.
@@ -65,16 +65,16 @@ Here is a sample Note this device emits:
| 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. |
+| [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. |
+| 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. |
+| [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.
+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
@@ -110,7 +110,7 @@ Mount each switch flush in the pill tray base directly beneath the path of the l
**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.
+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:**
@@ -129,17 +129,17 @@ Retain the LiPo inside the enclosure with a strip of hook-and-loop tape (Velcro)
## 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`.
+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.
+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.
+ **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.
@@ -149,7 +149,7 @@ Retain the LiPo inside the enclosure with a strip of hook-and-loop tape (Velcro)
|---|---|---|
| `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. |
+ | `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.
@@ -186,11 +186,11 @@ Within a minute of first power-on, the **Events** tab should start populating. T
## 7. Firmware Design
-The firmware is split across a main sketch and two helper files — all three live in [`firmware/cellular_medication_adherence_pillbox/`](firmware/cellular_medication_adherence_pillbox/):
+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).
+- **[`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
@@ -244,7 +244,7 @@ Before deploying to a battery-powered Notecarrier CX, **verify that `PILLBOX_BEN
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.
+**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.
@@ -283,7 +283,7 @@ The Notecard itself remains powered continuously and idles at approximately 8–
### 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.
+- `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).
@@ -304,7 +304,7 @@ 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.
+`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");
@@ -333,7 +333,7 @@ NotePayloadSaveAndSleep(&payload, state.poll_sec, NULL);

-**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.
+**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.
@@ -343,8 +343,8 @@ NotePayloadSaveAndSleep(&payload, state.poll_sec, NULL);
**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.
+- 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
@@ -357,7 +357,7 @@ NotePayloadSaveAndSleep(&payload, state.poll_sec, NULL);
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.
+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.
@@ -380,7 +380,7 @@ The daily summary is queued on the first wake after UTC midnight (or after the c
| 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.
+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.
@@ -388,7 +388,7 @@ A good Mojo trace on this device looks like: an essentially flat near-zero basel
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.
+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
@@ -409,13 +409,13 @@ If a problem isn't on this list, the [Blues community forum](https://discuss.blu
## 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.
+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.**
+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.
+**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.
@@ -424,7 +424,7 @@ The simplifications below are deliberate scope choices — each is a place where
**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.
+**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.
@@ -438,7 +438,7 @@ The simplifications below are deliberate scope choices — each is a place where
**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.
+**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.
@@ -450,7 +450,7 @@ Once a real RPM program is running the basic monitor, the following extensions a
**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 extend the outbound cadence as the LiPo voltage drops, maximizing runtime without manual intervention.
+**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.
diff --git a/87-post-discharge-vitals-relay-hub/README.md b/87-post-discharge-vitals-relay-hub/README.md
index 551cf5a4..e2f4071c 100644
--- a/87-post-discharge-vitals-relay-hub/README.md
+++ b/87-post-discharge-vitals-relay-hub/README.md
@@ -8,7 +8,7 @@ This reference application is intended to provide inspiration and help you get s
-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.
+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
@@ -18,18 +18,18 @@ This project is a [remote patient monitoring](https://blues.com/remote-patient-m
-**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 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.
+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 — 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.
+**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.
+**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.
+**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
@@ -38,11 +38,11 @@ This is the definition of a device that needs to work for everyone, including an
**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.
+**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.
+**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.
+**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
@@ -56,12 +56,12 @@ This is the definition of a device that needs to work for everyone, including an
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.
+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:
@@ -250,9 +250,9 @@ The four target service UUIDs are assigned by the Bluetooth SIG:
### 7.4 Characteristic value encoding
-The four measurement characteristics use three different wire encodings — it is not uniform SFLOAT throughout the stack.
+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 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.
@@ -311,11 +311,11 @@ Alert types: `weight_gain`, `bp_high`, `spo2_low`, `hr_high`, `hr_low`. The Note
### 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).
+**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.
+**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.
+**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).
@@ -348,7 +348,7 @@ do {
### 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 — essential for a care team that needs to act on a dangerous SpO2 within minutes, not the next quarter-hour.
+`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");
@@ -375,7 +375,7 @@ if (g_bpSvc.discover(connHandle)) {
### 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.
+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) {
@@ -428,9 +428,9 @@ static inline float parseSfloat(uint16_t raw) {
## 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.
+**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 — 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.
+**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.
@@ -441,9 +441,9 @@ static inline float parseSfloat(uint16_t raw) {
| 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.
+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).
+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
@@ -470,11 +470,11 @@ If a problem is not on this list, the [Blues community forum](https://discuss.bl
-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.
+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.**
+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.
@@ -482,11 +482,11 @@ The simplifications below are deliberate scope choices — each is a place where
**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.
+**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 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.
@@ -498,7 +498,7 @@ The simplifications below are deliberate scope choices — each is a place where
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.
+**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.
@@ -511,4 +511,4 @@ Taking this proof-of-concept toward a fieldable hub means hardening firmware del
## 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.
+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.
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 ee72d974..897e3cc4 100644
--- a/88-reefer-trailer-cold-chain-door-event-monitor/README.md
+++ b/88-reefer-trailer-cold-chain-door-event-monitor/README.md
@@ -8,7 +8,7 @@ This reference application is intended to provide inspiration and help you get s
-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.
+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.
@@ -19,18 +19,18 @@ This project is a [loss prevention](https://blues.com/loss-prevention/) referenc
**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.
+- **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.
+**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.
+[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.
@@ -39,20 +39,20 @@ Both failure modes have a common root: nobody can see inside the trailer while i

-**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.
+**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.
+**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.
+**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.
+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)
@@ -65,7 +65,7 @@ Both failure modes have a common root: nobody can see inside the trailer while i
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 minutes, then on the next outbound window (default ~60 minutes): batch of `trailer_log_cell.qo` Notes (one per 60 seconds sample), `trailer_summary_cell.qo` (hourly), and any alerts as `trailer_alert.qo` (immediate sync).
+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
@@ -134,9 +134,9 @@ All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notec
- 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 desensitisation. 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.
+> **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.
+> **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.
@@ -265,14 +265,14 @@ The alert Notefile uses [compact format](https://dev.blues.io/notecard/notecard-
### 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.)
+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:**
+**`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:**
+**`trailer_summary_cell.qo`: hourly, queued, cellular/WiFi only:**
```json
{
"t1_c": -1.4,
@@ -284,7 +284,7 @@ Three [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-b
}
```
-**`trailer_alert.qo` — immediate, enqueued then `hub.sync`, cellular or NTN transport:**
+**`trailer_alert.qo`: immediate, enqueued then `hub.sync`, cellular or NTN transport:**
```json
{
"alert": "temp_excursion",
@@ -295,17 +295,17 @@ Three [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-b
}
```
-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 required in the firmware.
+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.
+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.
+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.
@@ -472,7 +472,7 @@ Two anomaly patterns stand out immediately on a Mojo trace:
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 — temperature excursion and unauthorized door access — that drive most reefer trailer cargo loss. 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.
+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.
@@ -498,11 +498,11 @@ For anything not covered above, search or post on the [Blues community forum](ht
## 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.
+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.
+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**.
@@ -520,7 +520,7 @@ The simplifications below are deliberate scope choices — each marks a place wh
### 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.
+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.
@@ -537,6 +537,6 @@ Once the basic monitor is running across a real fleet, the following extensions
## 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 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.
diff --git a/89-pallet-attached-cold-chain-logger/README.md b/89-pallet-attached-cold-chain-logger/README.md
index 5f5cf8ec..443a371d 100644
--- a/89-pallet-attached-cold-chain-logger/README.md
+++ b/89-pallet-attached-cold-chain-logger/README.md
@@ -8,28 +8,28 @@ This reference application is intended to provide inspiration and help you get s
-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.
+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.
+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 — dynamically extending both summary interval and outbound sync cadence during long dwell periods to reduce satellite session frequency and NTN data cost, and emitting an immediate state-change Note whenever the shipment transitions between states.
+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.
+**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.
+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.
+**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.
+**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
@@ -45,20 +45,20 @@ This project uses [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/prod
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.
+**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.
+**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.
+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.
@@ -304,7 +304,7 @@ Open the serial monitor at **115200 baud** to watch `[cargo]` log lines. On the
### 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 — important 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.
+- **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.
@@ -313,7 +313,7 @@ Open the serial monitor at **115200 baud** to watch `[cargo]` log lines. On the
**`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 — 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_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.
@@ -417,7 +417,7 @@ NotePayloadSaveAndSleep(&save, gSampleSec, NULL);
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.
+- 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.
@@ -510,19 +510,19 @@ For satellite-reliant lanes, the primary lever is `dwell_batch_factor` — it dy
## 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.
+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.
+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.
+**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.
+**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.
@@ -547,4 +547,4 @@ Once the basic logger is proven on real shipments, the following extensions are
## 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.
+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.