diff --git a/51-rooftop-hvac-predictive-maintenance/README.md b/51-rooftop-hvac-predictive-maintenance/README.md index 3d6bc550..7122fd93 100644 --- a/51-rooftop-hvac-predictive-maintenance/README.md +++ b/51-rooftop-hvac-predictive-maintenance/README.md @@ -92,7 +92,7 @@ All Blues parts ship with an embedded SIM including 500 MB of data and 10 years ![Wiring diagram: thermistor dividers on A0/A1, CT bias circuit on A2, SDP810 on I²C, power chain from 120 VAC → 5 V supply → Mojo → +VBAT](diagrams/02-wiring-assembly.svg) -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. The Notecard Cell+WiFi (MBGLW) seats into the carrier's M.2 slot; cellular, GNSS, and WiFi antennas are u.FL leads to roof-mounted externals. The Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad, and reports cumulative mAh to the Notecard over its [Qwiic](https://www.sparkfun.com/qwiic) connector. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi (MBGLW) seats into the carrier's M.2 slot; cellular, GNSS, and WiFi antennas are u.FL leads to roof-mounted externals. The Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad, and reports cumulative mAh to the Notecard over its [Qwiic](https://www.sparkfun.com/qwiic) connector. Pin-by-pin: diff --git a/53-municipal-wastewater-lift-station-monitor/README.md b/53-municipal-wastewater-lift-station-monitor/README.md index 60e71a7b..53613f3f 100644 --- a/53-municipal-wastewater-lift-station-monitor/README.md +++ b/53-municipal-wastewater-lift-station-monitor/README.md @@ -125,7 +125,7 @@ Here is a sample Note this device emits: -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin headers. Notecard for Skylo seats into the M.2 slot. Its `MAIN` u.FL port connects to the included Skylo-certified antenna, which carries both the cellular and satellite signals — connect it directly, or route it through a u.FL-to-SMA-F bulkhead pigtail in the enclosure wall if you are using an external SMA mag-mount. Its `GPS` u.FL port connects to the passive GPS/GNSS antenna. Both antennas mount outdoors with a clear sky view (northern hemisphere: the southern sky), so the unit can reach the Skylo satellite network wherever it falls back from cellular. The Mojo connects via the [Qwiic](https://www.sparkfun.com/qwiic) connector on the Notecarrier and sits inline between the 5 V supply and the Notecarrier's +VBAT pad during bench validation. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin headers. Notecard for Skylo seats into the M.2 slot. Its `MAIN` u.FL port connects to the included Skylo-certified antenna, which carries both the cellular and satellite signals — connect it directly, or route it through a u.FL-to-SMA-F bulkhead pigtail in the enclosure wall if you are using an external SMA mag-mount. Its `GPS` u.FL port connects to the passive GPS/GNSS antenna. Both antennas mount outdoors with a clear sky view (northern hemisphere: the southern sky), so the unit can reach the Skylo satellite network wherever it falls back from cellular. The Mojo connects via the [Qwiic](https://www.sparkfun.com/qwiic) connector on the Notecarrier and sits inline between the 5 V supply and the Notecarrier's +VBAT pad during bench validation. **Level sensor (4–20 mA current loop)** diff --git a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/README.md b/54-multi-site-walk-in-cooler-energy-setpoint-monitor/README.md index 46e251b4..6eea23f7 100644 --- a/54-multi-site-walk-in-cooler-energy-setpoint-monitor/README.md +++ b/54-multi-site-walk-in-cooler-energy-setpoint-monitor/README.md @@ -142,7 +142,7 @@ All Blues hardware ships with an active SIM including 500 MB of data and 10 year -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. The Notecard Cell+WiFi seats into the carrier's M.2 slot. **An external antenna is mandatory in a metal walk-in box or steel mechanical-room cabinet** — a Notecard antenna inside a metal enclosure cannot radiate reliably. Route a short u.FL-to-SMA pigtail from the Notecard's u.FL port through a cable gland in the NEMA 4X enclosure wall and attach it to the panel-mount SMA whip antenna on the exterior. The Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad for bench validation. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi seats into the carrier's M.2 slot. **An external antenna is mandatory in a metal walk-in box or steel mechanical-room cabinet** — a Notecard antenna inside a metal enclosure cannot radiate reliably. Route a short u.FL-to-SMA pigtail from the Notecard's u.FL port through a cable gland in the NEMA 4X enclosure wall and attach it to the panel-mount SMA whip antenna on the exterior. The Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad for bench validation. Pin-by-pin connections: diff --git a/56-commercial-plug-load-after-hours-waste-dashboard/README.md b/56-commercial-plug-load-after-hours-waste-dashboard/README.md index 33982529..64828159 100644 --- a/56-commercial-plug-load-after-hours-waste-dashboard/README.md +++ b/56-commercial-plug-load-after-hours-waste-dashboard/README.md @@ -116,7 +116,7 @@ All Blues hardware ships with an active SIM including 500 MB of data and 10 year ![Wiring: 4 SCT-013 CT clamps to A0–A3 with shared 10 kΩ/10 kΩ + 10 µF bias node; external SMA antenna; 120 VAC → IRM-10-5 (5 V) → Mojo → +VBAT power chain](diagrams/02-wiring-assembly.svg) -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. The Notecard Cell+WiFi seats in the carrier's M.2 slot. Mojo sits inline between the 5 V supply output and the Notecarrier's `+VBAT` pad. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi seats in the carrier's M.2 slot. Mojo sits inline between the 5 V supply output and the Notecarrier's `+VBAT` pad. diff --git a/58-commercial-grease-interceptor-level-monitor/README.md b/58-commercial-grease-interceptor-level-monitor/README.md index f3587df9..ae318098 100644 --- a/58-commercial-grease-interceptor-level-monitor/README.md +++ b/58-commercial-grease-interceptor-level-monitor/README.md @@ -10,7 +10,7 @@ This reference application is intended to provide inspiration and help you get s This project is a [truck-roll reduction](https://blues.com/truck-roll-reduction/) device for pumping providers who service **commercial grease interceptors**. A waterproof ultrasonic distance sensor installed in the access cover reports fill level over cellular every 15 minutes; when the level reaches a threshold, an alert dispatches immediately to the [Blues Notehub](https://blues.com/notehub/) cloud service. Trucks are routed on actual condition, not a fixed calendar. -**What you'll have at the end:** A weatherproof wall-mounted enclosure with a Notecarrier CX + Notecard Cell+WiFi that samples a DFRobot ultrasonic sensor every 15 minutes, publishes daily summaries and threshold alerts to Notehub, and lets you tune alert thresholds and sample intervals via fleet-level environment variables without re-flashing. +**What you'll have at the end:** A weatherproof wall-mounted enclosure with a Notecarrier CX + Notecard Cell+WiFi and an inline Blues Mojo coulomb counter that samples a DFRobot ultrasonic sensor every 15 minutes, publishes daily summaries (including per-window energy consumption) and threshold alerts to Notehub, and lets you tune alert thresholds and sample intervals via fleet-level environment variables without re-flashing. **This reference implementation is scoped to hydromechanical (HGI) and batch-collection interceptors without a fixed outlet weir** — geometries where the liquid surface rises predictably with FOG accumulation, making fill percentage a direct proxy for pump-out urgency. See §1 and §10 before deploying on a conventional constant-level gravity interceptor where a fixed weir holds the surface height independent of FOG-layer depth. @@ -18,7 +18,7 @@ This project is a [truck-roll reduction](https://blues.com/truck-roll-reduction/ **The problem.** Every commercial kitchen is legally required to install a **grease interceptor** (sometimes called a grease trap) — a chamber plumbed into the kitchen drain line that intercepts **FOG** (fats, oils, and grease) before it enters the municipal sewer. FOG accumulates as a floating layer at the top of the interceptor. Left unchecked, that layer eventually reaches the inlet pipe and starts flowing into the sewer, a violation that can result in fines, shutdowns, and backups into the kitchen floor drains. -To stay compliant, **FSEs** (food service establishments, restaurants, cafeterias, ghost kitchens, institutional foodservice) hire a pumping provider who periodically vacuums the trap clean. The industry default is a fixed service cadence: the truck shows up every four weeks, or every two weeks, regardless of how full the trap actually is. In practice, a slow-season week produces far less grease than a holiday rush. Fixed cadence means the pumper sometimes arrives when the interceptor is only 30% full — a truck roll that did nothing for compliance, and sometimes arrives after the interceptor has already overflowed. Neither outcome is good, and the FSE pays for every dispatch. +To stay compliant, **FSEs** (food service establishments, restaurants, cafeterias, ghost kitchens, institutional food service) hire a pumping provider who periodically vacuums the trap clean. The industry default is a fixed service cadence: the truck shows up every four weeks, or every two weeks, regardless of how full the trap actually is. In practice, a slow-season week produces far less grease than a holiday rush. Fixed cadence means the pumper sometimes arrives when the interceptor is only 30% full — a truck roll that did nothing for compliance, and sometimes arrives after the interceptor has already overflowed. Neither outcome is good, and the FSE pays for every dispatch. A fill-level sensor changes the economics entirely. With a real-time reading, the pumping provider dispatches trucks on condition rather than on schedule. Interceptors that fill quickly (high-volume kitchens, fryer-heavy menus) get serviced before they overflow; interceptors that fill slowly get serviced less often without the compliance risk. The result is fewer unnecessary truck rolls, better route efficiency, and a condition-based service story the pumper can offer as a premium tier. **This reference design targets hydromechanical (HGI) and batch-collection interceptors without a fixed outlet weir — geometries where the top liquid surface rises predictably as FOG and wastewater accumulate, so a rising fill percentage is a meaningful proxy for pump-out urgency.** For deployments on conventional constant-level gravity interceptors — where an outlet weir holds the liquid surface nearly fixed regardless of FOG-layer depth. See the measurement-model Note below and §10. @@ -38,7 +38,7 @@ A fill-level sensor changes the economics entirely. With a real-time reading, th ![System architecture: Interceptor → A02YYUW probe → Edge enclosure (Notecarrier CX + Cygnet + Notecard) → Notehub → Routes](diagrams/01-system-architecture.svg) -**Device-side responsibilities.** Inside the wall-mounted enclosure, the Cygnet STM32L433 host on the Notecarrier CX wakes every 15 minutes (configurable), reads the ultrasonic probe over hardware UART, converts the median distance to a fill percentage, and checks the alert threshold. When it's done, it hands any resulting Note to the Notecard over I²C and uses [`NotePayloadSaveAndSleep`](https://dev.blues.io/guides-and-tutorials/notecard-guides/feather-mcu-low-power-management/) to serialize its runtime state — running fill total, peak fill, last alert and report timestamps — into Notecard flash and cut its own power. The next wake comes up fresh, rehydrates the state, and picks up where it left off; no JSON marshaling, no AT commands ever touched the wire. +**Device-side responsibilities.** Inside the wall-mounted enclosure, the Cygnet STM32L433 host on the Notecarrier CX wakes every 15 minutes (configurable), reads the ultrasonic probe over hardware UART, converts the median distance to a fill percentage, and checks the alert threshold. Once per daily summary window it also reads the Mojo coulomb counter via [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power) to capture the energy consumed that window. When it's done, it hands any resulting Note to the Notecard over I²C and uses [`NotePayloadSaveAndSleep`](https://dev.blues.io/guides-and-tutorials/notecard-guides/feather-mcu-low-power-management/) to serialize its runtime state — running fill total, peak fill, last alert and report timestamps — into Notecard flash and cut its own power. The next wake comes up fresh, rehydrates the state, and picks up where it left off; no JSON marshaling, no AT commands ever touched the wire. **Notecard responsibilities.** The Notecard holds Notes in its local queue and runs two independent schedules configured by [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set): an **outbound** cadence (default every 24 hours) for flushing queued [Notes](https://dev.blues.io/api-reference/glossary/#note) to Notehub, and a fixed **inbound** cadence (every 2 hours, set by `HUB_INBOUND_MIN`) for polling Notehub for updated environment variables. Anything tagged `sync:true` skips both windows and opens an immediate session. The same [environment variable](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) mechanism is how a pumping provider retunes interceptor depth, alert threshold, and sampling cadence across an entire route from a browser — no reflash, no site visit. @@ -67,7 +67,8 @@ Here is a sample Note this device emits: "fill_pct_avg": 42.3, "fill_pct_peak": 51.7, "fill_pct_now": 44.1, - "valid_samples": 94 + "valid_samples": 94, + "power_mah": 248.6 } } ``` @@ -78,7 +79,8 @@ Here is a sample Note this device emits: |------|-----|-----------| | [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an onboard Cygnet STM32L433 host MCU — no separate host board needed. ATTN pin wiring to control the host power rail is built in. | | [Notecard Cell+WiFi (MBGLW)](https://shop.blues.com/products/notecard-cell-wifi?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([Datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | 1 | Cellular-first connectivity removes per-site WiFi dependency; WiFi fallback available for installations in utility rooms with accessible APs. Ships with a prepaid global SIM. | -| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter on the +VBAT rail for ground-truth energy validation during commissioning. | +| [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | **Required, permanently installed.** LTC2959 coulomb counter wired inline on the +VBAT rail and connected to the Notecarrier CX Qwiic port. Notecard firmware (v8.1.3+) auto-detects it over I²C; the firmware reads per-window energy via [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power) and reports it as `power_mah` in every daily summary (see §9). | +| [Qwiic cable](https://www.sparkfun.com/qwiic) (4-pin JST-SH, 50–100 mm) | 1 | Connects the Mojo to the Notecarrier CX Qwiic port so the Notecard can read the coulomb counter over I²C. A short cable ships with most Mojo units; confirm before ordering. | | [DFRobot A02YYUW Waterproof Ultrasonic Sensor (SEN0311)](https://www.dfrobot.com/product-1935.html) | 1 | IP67-rated ultrasonic probe with UART output; range 3–450 cm; average current ≤8 mA. The IP67 rating covers ingress protection against dust and temporary water immersion — it establishes resistance to splash and condensation but does **not** address long-term chemical compatibility with grease, H₂S, sewer gas condensate, or cleaning chemicals found in interceptor atmospheres. Chemical durability in that environment is unverified for this sensor; field-validate before committing to a deployment, or substitute a sensor with explicit chemical-resistance ratings for the target environment. | | [Hammond 1554W2GY](https://www.hammfg.com/part/1554W2GY) enclosure, 180 × 180 × 66 mm, IP68 polycarbonate | 1 | Sized for the Notecarrier CX (≈80 × 56 mm footprint) with room for the Mojo and cable management. | | [SparkFun CEL-16432 LTE Hinged External Antenna](https://www.sparkfun.com/lte-hinged-external-antenna-698mhz-2-7ghz-sma-male.html), SMA male, 698 MHz–2.7 GHz | 1 | Hinged external antenna mounted on the polycarbonate enclosure via an SMA bulkhead — covers all MBGLW LTE Cat-1 bis bands across the 698 MHz–2.7 GHz range. | @@ -101,16 +103,17 @@ All Blues hardware ships with an active SIM including 500 MB of data and 10 year -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. The Notecard Cell+WiFi seats into the CX's M.2 slot; the SparkFun CEL-16432 hinged LTE antenna mounts externally on the polycarbonate enclosure via an SMA bulkhead connector. **The Blues Mojo coulomb counter is bench-validation equipment only** — it is not part of the deployed field unit. The firmware does not read Mojo data over I²C; Mojo functions purely as a hardware current integrator on the +VBAT rail during commissioning (see §9). +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi seats into the CX's M.2 slot; the SparkFun CEL-16432 hinged LTE antenna mounts externally on the polycarbonate enclosure via an SMA bulkhead connector. **The Blues Mojo coulomb counter is a permanent part of the deployed unit.** It is wired inline on the +VBAT rail (so it integrates the whole-board current) and connected to the Notecarrier CX Qwiic port (so the Notecard can read it over I²C). The Notecard auto-detects the Mojo and the firmware reports per-window energy consumption (`power_mah`) in every daily summary via [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power) — see §9 for the wiring detail and validation procedure. The A02YYUW sensor has a 2-meter cable terminating in a 4-pin PH2.0 connector: **VCC** (red), **GND** (black), **RX** (yellow, mode-select), and **TX** (white, UART data out). **Cut off the PH2.0 connector** — the molded housing is wider than the M16 enclosure gland's bore and will not pass through. Strip 8 mm from each wire, feed the four bare ends through the enclosure's M16 cable gland from outside, tighten the compression nut on the cable jacket, then fit a female 2.54 mm DuPont crimp contact onto each stripped lead and seat them in a 4-pin housing (or use individual housings). This terminated end lands on the Notecarrier CX's 2.54 mm header as described below. Pin-by-pin: -- **+3V3\_OUT** → A02YYUW **VCC** (red). `+3V3_OUT` is the exact label on the Notecarrier CX v1.3 dual 16-pin header; it supplies 3.3 V at up to 100 mA. The sensor draws ≤8 mA average, well within that limit. +- **+3V3\_OUT** → A02YYUW **VCC** (red). `+3V3_OUT` is the exact label on the Notecarrier CX dual 16-pin header; it supplies 3.3 V at up to 100 mA. The sensor draws ≤8 mA average, well within that limit. - **GND** → A02YYUW **GND** (black) and A02YYUW **RX** (yellow). Tying the sensor's RX pin to GND selects real-time continuous output mode — the sensor streams packets as fast as it measures. Leave the RX pin floating as an alternative (the internal pull-down produces the same behavior). -- **RX** header pin → A02YYUW **TX** (white). `RX` is the exact label on the Notecarrier CX v1.3 header for the Cygnet's hardware UART receive line. The firmware assigns `Serial1` to this UART instance when built for the Notecarrier CX's onboard Cygnet — this mapping is correct for the standard target. If you retarget the sketch to a different STM32 board or core variant, confirm that `Serial1` maps to the `RX`/`TX` header pins for that board before wiring. -- **+VBAT** pad → positive lead (red or marked +5V) from the DC wall adapter pigtail. The negative lead connects to `GND`. **Do not connect the CX's USB-C port to any power source during normal operation** — VUSB must remain absent for the Notecard to reach its lowest idle power floor (~8 µA). *During bench testing only:* splice Mojo inline between the adapter and `+VBAT` as described in §9. +- **RX** header pin → A02YYUW **TX** (white). `RX` is the exact label on the Notecarrier CX header for the Cygnet's hardware UART receive line. The firmware assigns `Serial1` to this UART instance when built for the Notecarrier CX's onboard Cygnet — this mapping is correct for the standard target. If you retarget the sketch to a different STM32 board or core variant, confirm that `Serial1` maps to the `RX`/`TX` header pins for that board before wiring. +- **+VBAT** pad → the Mojo's output, with the Mojo spliced inline on the positive rail: DC wall adapter positive lead (red or marked +5V) → Mojo input; Mojo output → CX `+VBAT` pad. The adapter negative lead connects to `GND`. This routes all board current through the Mojo so it measures whole-board draw. **Do not connect the CX's USB-C port to any power source during normal operation** — VUSB must remain absent for the Notecard to reach its lowest idle power floor (~8 µA). See §9 for the exact Mojo terminal labels. +- **Qwiic port** → Mojo Qwiic connector. A 4-pin JST-SH Qwiic cable from the Notecarrier CX Qwiic port to the Mojo lets the Notecard read the coulomb counter over I²C. No other configuration is needed — the Notecard auto-detects the Mojo. **Power entry.** The DC wall adapter plugs into a standard wall outlet outside the enclosure; only the low-voltage DC pigtail enters the box. Drill an M16 clearance hole in the enclosure wall for the DC cable; fit the M16 gland body from outside and secure with its locknut from inside. Thread the DC pigtail through the gland from outside and tighten the compression nut on the cable jacket to grip and strain-relieve it. Inside the enclosure, trim the leads to length, strip 8 mm of insulation from each, and connect the positive lead to the CX `+VBAT` pad and the negative lead to `GND` (use female 2.54 mm DuPont crimp contacts or solder directly to the header pads). If your adapter has a 2.1 mm barrel-jack output, use a short barrel-to-DuPont pigtail adapter and connect as above. There is no mains voltage inside the enclosure; no insulation clearance, creepage, or earth-bonding requirements apply to the DC wiring. @@ -199,7 +202,7 @@ The A02YYUW streams 4-byte UART packets continuously at 9600 baud: `[0xFF][high] ### Event payload design -One [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) daily summary Note (`grease_summary.qo`), plus an untemplated immediate alert (`grease_alert.qo`) whenever `fill_pct` is at or above the alert threshold and the 1-hour cooldown has elapsed — which means a new alert fires every cooldown interval for as long as the interceptor remains above threshold. The template fixes each summary to a compact binary record (approximately 28 bytes on the wire), a meaningful saving over a full deployment lifetime with a sensor that's been installed for years in hundreds of interceptors. See "Example payloads routed to Notehub" below (§8) for the payload shape. +One [template-backed](https://dev.blues.io/notecard/notecard-walkthrough/low-bandwidth-design#working-with-note-templates) daily summary Note (`grease_summary.qo`), plus an immediate alert (`grease_alert.qo`) whenever `fill_pct` is at or above the alert threshold and the 1-hour cooldown has elapsed — which means a new alert fires every cooldown interval for as long as the interceptor remains above threshold. The template fixes each summary to a compact binary record (approximately 32 bytes on the wire), a meaningful saving over a full deployment lifetime with a sensor that's been installed for years in hundreds of interceptors. See "Example payloads routed to Notehub" below (§8) for the payload shape. ### Low-power strategy @@ -238,6 +241,7 @@ JAddNumberToObject(body, "fill_pct_avg", TFLOAT32); JAddNumberToObject(body, "fill_pct_peak", TFLOAT32); JAddNumberToObject(body, "fill_pct_now", TFLOAT32); JAddNumberToObject(body, "valid_samples", TUINT32); +JAddNumberToObject(body, "power_mah", TFLOAT32); // mAh consumed this window (Mojo) notecard.sendRequest(req); ``` @@ -288,7 +292,7 @@ Every 15 minutes the Cygnet host wakes, fires the sensor five times, takes the m ### Example payloads routed to Notehub -**Daily summary** (one per `report_interval_min`, templated, ~28 bytes): +**Daily summary** (one per `report_interval_min`, templated, ~32 bytes): ```json { "file": "grease_summary.qo", @@ -296,7 +300,8 @@ Every 15 minutes the Cygnet host wakes, fires the sensor five times, takes the m "fill_pct_avg": 42.3, "fill_pct_peak": 51.7, "fill_pct_now": 44.1, - "valid_samples": 94 + "valid_samples": 94, + "power_mah": 248.6 } } ``` @@ -340,7 +345,7 @@ Compare this value against your tape measurement from sensor face to the liquid **Alert simulation.** The fastest way to confirm the alert path end-to-end: in Notehub, lower `alert_threshold_pct` to `5` in the device's environment variables. On the next inbound sync (within 2 hours at the default `HUB_INBOUND_MIN`), the device will pull the new value. At the next wake, the current fill percentage will almost certainly exceed 5%, and a `grease_alert.qo` Note will arrive in Notehub within a session-establishment window of the sample time. -**Using Mojo to validate power behavior.** Mojo is bench-only equipment for this project; it is not installed in the deployed field unit and the firmware does not read Mojo data. To use Mojo for power validation: splice it inline on the `+VBAT` rail between the 5 V DC wall adapter and the CX `+VBAT` pad (adapter positive lead → Mojo `BAT`; Mojo `LOAD` → CX `+VBAT` pad; adapter negative → CX `GND`). Leave the unit running for at least one full `report_interval_min` period. Because Mojo is inserted at the `+VBAT` rail, its readings reflect the **whole-board** draw — Notecard plus the CX's onboard regulators, quiescent biasing, and the continuously-powered A02YYUW sensor, not the Notecard in isolation. +**Mojo wiring and energy reporting.** The Mojo is a permanent part of the deployed unit. Wire it inline on the `+VBAT` rail between the 5 V DC wall adapter and the CX `+VBAT` pad (adapter positive lead → Mojo `BAT`; Mojo `LOAD` → CX `+VBAT` pad; adapter negative → CX `GND`), and connect the Mojo to the Notecarrier CX Qwiic port with a 4-pin JST-SH Qwiic cable. Because Mojo is inserted at the `+VBAT` rail, its readings reflect the **whole-board** draw — Notecard plus the CX's onboard regulators, quiescent biasing, and the continuously-powered A02YYUW sensor, not the Notecard in isolation. Notecard firmware v8.1.3 or later auto-detects the Mojo over I²C; no extra configuration is required. Each daily summary cycle the firmware calls [`card.power`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-power), reads the accumulated `milliamp_hours`, reports it as `power_mah` in the `grease_summary.qo` Note, then resets the counter so each summary reports the energy consumed during that one window. A `power_mah` of `-1` in a summary means the Mojo reading was unavailable for that window (check the Qwiic connection and the Notecard firmware version). To inspect live values during commissioning, issue `{"req":"card.power"}` in the in-browser Notecard Playground or over the USB serial connection and confirm a plausible `milliamp_hours` and `voltage`. The table below shows the published MBGLW Notecard figures alongside the trace shapes to look for and confirm on your assembled unit. Board-level numbers beyond the Notecard's own idle are assembly-dependent and should be validated empirically with Mojo rather than assumed from the figures below. @@ -350,7 +355,7 @@ The table below shows the published MBGLW Notecard figures alongside the trace s | Host active (~2 seconds sampling cycle) | Notecard remains near idle; host MCU and UART activity are additional load | Brief current step above the quiescent floor every `sample_interval_sec` (default 15 minutes); the A02YYUW draw is already in the floor (sensor is always powered) — Cygnet wake adds its own incremental draw on top | | Notecard cellular session (LTE Cat-1 bis) | ~250 mA average, ≤2 A peak during transmit bursts ([MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-mbglw/)) | **Outbound** sync: 30–60 seconds elevation once per `report_interval_min` (default once per day). **Inbound** polls: shorter sessions every 2 hours (`HUB_INBOUND_MIN = 120`) — up to 12 additional radio wakes per day at default settings. Each inbound session has a radio-wake and session-establishment cost even though no summary data is delivered; at default cadence the 12 daily inbound sessions may collectively rival or exceed the single outbound session in energy. Budget both when sizing supply capacity. A continuously powered sensor (~8 mA × 24 h ≈ 192 mAh/day) likely remains the dominant daily energy consumer unless the `+3V3_OUT` rail is cut during host sleep — confirm with Mojo (splice between the DC wall adapter and CX `+VBAT` as described above). | -The dominant daily energy consumer is likely the always-on sensor (~8 mA continuous ≈ 192 mAh/day) unless the `+3V3_OUT` rail is cut during host sleep — which the CX datasheet does not document. The cellular budget is composed of both the outbound summary sync (one per `report_interval_min`) and the inbound environment-variable polls (up to 12 per day at the default 2-hour cadence); at default settings the aggregated inbound sessions may collectively approach or exceed the energy of the single outbound sync. On a correctly-behaving Mojo trace you should see: a roughly constant quiescent floor (Notecard idle + always-on sensor), a brief incremental step every 15 minutes when the host is active, a pattern of shorter radio-wake spikes approximately every 2 hours (inbound polls), and a larger, longer cellular sync spike once per `report_interval_min` (outbound). If empirically the quiescent floor drops sharply between the 15-minute host-active steps, the sensor rail is being cut during sleep — confirm either way before sizing a battery-backed variant. If instead you see sustained elevated current with no sleeping pattern, the host is not sleeping — troubleshoot as follows for the Notecarrier CX: (1) confirm `NotePayloadSaveAndSleep` is actually reached (Notecard API debug output on the USB serial monitor stops after a successful sleep call); (2) confirm the `card.attn` request returns without an error in that same debug stream; (3) the Notecarrier CX switches the Cygnet host power rail via its built-in ATTN-controlled power circuit — no external EN pin wiring is needed or expected on this carrier. When bench testing is complete, remove Mojo and wire the DC adapter pigtail directly to `+VBAT` / `GND` for the deployed unit. +The dominant daily energy consumer is likely the always-on sensor (~8 mA continuous ≈ 192 mAh/day) unless the `+3V3_OUT` rail is cut during host sleep — which the CX datasheet does not document. The cellular budget is composed of both the outbound summary sync (one per `report_interval_min`) and the inbound environment-variable polls (up to 12 per day at the default 2-hour cadence); at default settings the aggregated inbound sessions may collectively approach or exceed the energy of the single outbound sync. On a correctly-behaving Mojo trace you should see: a roughly constant quiescent floor (Notecard idle + always-on sensor), a brief incremental step every 15 minutes when the host is active, a pattern of shorter radio-wake spikes approximately every 2 hours (inbound polls), and a larger, longer cellular sync spike once per `report_interval_min` (outbound). If empirically the quiescent floor drops sharply between the 15-minute host-active steps, the sensor rail is being cut during sleep — confirm either way before sizing a battery-backed variant. If instead you see sustained elevated current with no sleeping pattern, the host is not sleeping — troubleshoot as follows for the Notecarrier CX: (1) confirm `NotePayloadSaveAndSleep` is actually reached (Notecard API debug output on the USB serial monitor stops after a successful sleep call); (2) confirm the `card.attn` request returns without an error in that same debug stream; (3) the Notecarrier CX switches the Cygnet host power rail via its built-in ATTN-controlled power circuit — no external EN pin wiring is needed or expected on this carrier. The Mojo remains wired inline on `+VBAT` and connected over Qwiic in the deployed unit, so the same energy trace is available in the field through the `power_mah` field of each daily summary. ## 10. Limitations and Next Steps @@ -378,8 +383,6 @@ The simplifications below are deliberate scope choices — each keeps the build **Alert cooldown is per-device, not per-channel.** The 1-hour alert cooldown is enforced in firmware. If the downstream routing endpoint needs its own deduplication or escalation logic (e.g., "send a second alert if not acknowledged within 4 hours"), that must be implemented in the Notehub route or the receiving system. -**Mojo is bench-validation equipment here.** The firmware does not read the Mojo's LTC2959 coulomb counter over I²C. Adding a `mojo_mah` field to the daily summary is a one-function extension if fleet-level energy telemetry is useful in production. - ### Production Next Steps Once the basic level monitor is running in the field, the following extensions are the natural progression — roughly from per-site usability to deeper measurement and fleet management. diff --git a/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor.ino b/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor.ino index db31d988..4a39b80d 100644 --- a/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor.ino +++ b/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor.ino @@ -255,12 +255,22 @@ void setup() { if (report_due) { if (state.valid_samples > 0) { - if (sendSummary(state)) { + // Read the energy consumed this window from the Mojo coulomb counter + // (via card.power) before building the summary. Returns -1.0 if the + // Mojo reading is unavailable; that sentinel is carried in the note. + float power_mah = readPowerMah(); + if (sendSummary(state, power_mah)) { // Reset window accumulators only after confirmed delivery so a // failed send retries on the next wake with the full window intact. state.fill_pct_sum = 0.0f; state.valid_samples = 0; state.fill_pct_peak = 0.0f; + // Zero the Mojo counter so the next window starts fresh, but only + // when this window's reading was valid — a transient card.power + // failure rolls the energy into the next window instead of losing it. + if (power_mah >= 0.0f) { + resetPowerCounter(); + } state.last_report_epoch = (now > 0) ? now : 1; // 1 = fired; time not yet known } } else if (state.last_report_epoch > 0 && now > 0) { @@ -359,6 +369,7 @@ static bool defineTemplates(void) { JAddNumberToObject(body, "fill_pct_peak", TFLOAT32); JAddNumberToObject(body, "fill_pct_now", TFLOAT32); JAddNumberToObject(body, "valid_samples", TUINT32); // 4-byte unsigned integer + JAddNumberToObject(body, "power_mah", TFLOAT32); // mAh consumed this window (Mojo) J *rsp = notecard.requestAndResponse(req); bool ok = notecardResponseOk(rsp); notecard.deleteResponse(rsp); diff --git a/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.cpp b/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.cpp index dbdb994b..dc83116f 100644 --- a/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.cpp +++ b/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.cpp @@ -122,6 +122,48 @@ uint32_t getEpochTime(void) { return t; } +// =========================================================================== +// Read accumulated energy from the Mojo coulomb counter via card.power. +// +// The Blues Mojo is wired inline on the +VBAT rail and connected to the +// Notecarrier CX Qwiic port. Notecard firmware v8.1.3+ auto-detects the Mojo +// (an Analog Devices LTC2959 coulomb counter) over I2C and exposes its data +// through card.power. The LTC2959 integrates charge continuously in hardware, +// so an on-demand read returns the cumulative mAh consumed since the last +// reset — no logging cadence needs to be configured. +// +// Returns the cumulative milliamp-hours, or -1.0 if the Notecard returns an +// error (e.g. no Mojo attached or Notecard firmware too old). The caller +// treats a negative result as "reading unavailable" and does not reset the +// counter, so that window's energy rolls into the next window rather than +// being discarded. +// =========================================================================== +float readPowerMah(void) { + J *rsp = notecard.requestAndResponse(notecard.newRequest("card.power")); + if (!notecardResponseOk(rsp)) { + notecard.deleteResponse(rsp); + return -1.0f; + } + float mah = (float)JGetNumber(rsp, "milliamp_hours"); + notecard.deleteResponse(rsp); + return mah; +} + +// =========================================================================== +// Reset the Mojo coulomb counter back to zero so the next summary window +// measures only the energy consumed during that window. Called only after a +// summary note is confirmed delivered AND the preceding read succeeded. +// A failure here is non-fatal: it is logged, and the next window simply +// reports the accumulated total since the last successful reset. +// =========================================================================== +void resetPowerCounter(void) { + J *req = notecard.newRequest("card.power"); + JAddBoolToObject(req, "reset", true); + J *rsp = notecard.requestAndResponse(req); + notecardResponseOk(rsp); // logs on failure; non-fatal + notecard.deleteResponse(rsp); +} + // =========================================================================== // Validate a Notecard response: non-NULL and no err field present. // Logs the Notecard error string when debug output is enabled. @@ -155,11 +197,15 @@ bool notecardResponseOk(J *rsp) { // be zero. Notecard templates default to omitempty at Notehub serialization, // stripping any field whose value is 0/false/null/"". Without "full":true a // freshly pumped-out interceptor (0 % fill) would have fill_pct_avg, -// fill_pct_peak, and fill_pct_now all silently dropped from the Notehub body -// — only valid_samples would appear, leaving the consumer unable to -// distinguish "0 % fill" from "field never sent". +// fill_pct_peak, fill_pct_now, and power_mah all silently dropped from the +// Notehub body — only valid_samples would appear, leaving the consumer unable +// to distinguish "0 % fill" / "0 mAh" from "field never sent". +// +// power_mah carries the energy consumed during this window, read from the +// Mojo coulomb counter via card.power. A negative value (-1.0) signals that +// the Mojo reading was unavailable for this window. // =========================================================================== -bool sendSummary(const State &state) { +bool sendSummary(const State &state, float power_mah) { float avg = state.fill_pct_sum / (float)state.valid_samples; J *req = notecard.newRequest("note.add"); @@ -175,6 +221,7 @@ bool sendSummary(const State &state) { // reach this point, so we can include it unconditionally. JAddNumberToObject(body, "fill_pct_now", state.fill_pct_last_valid); JAddNumberToObject(body, "valid_samples", state.valid_samples); + JAddNumberToObject(body, "power_mah", power_mah); J *rsp = notecard.requestAndResponse(req); bool ok = notecardResponseOk(rsp); notecard.deleteResponse(rsp); diff --git a/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.h b/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.h index 236ca81d..3327d941 100644 --- a/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.h +++ b/58-commercial-grease-interceptor-level-monitor/firmware/grease_interceptor_monitor/grease_interceptor_monitor_helpers.h @@ -84,6 +84,10 @@ float readDistanceMm(void); float medianOf(float *arr, int n); float distanceToFillPct(float distance_mm, float depth_mm); uint32_t getEpochTime(void); +float readPowerMah(void); // mAh consumed since last reset, via card.power + // (Notecard auto-detects the Mojo/LTC2959 over + // Qwiic); returns -1.0 if the reading is unavailable +void resetPowerCounter(void); // zero the Mojo coulomb counter for the next window bool notecardResponseOk(J *rsp); -bool sendSummary(const State &state); +bool sendSummary(const State &state, float power_mah); bool sendAlert(float fill_pct, float threshold_pct); diff --git a/60-lone-worker-panic-fall-detection-beacon/README.md b/60-lone-worker-panic-fall-detection-beacon/README.md index f156d7f8..dd6ac387 100644 --- a/60-lone-worker-panic-fall-detection-beacon/README.md +++ b/60-lone-worker-panic-fall-detection-beacon/README.md @@ -151,7 +151,7 @@ Notecard for Skylo ships with an active global SIM including 500 MB of cellular ![Wiring: LIS3DH + DRV2605L on shared I²C; panic button to D9 INPUT_PULLUP; Notecard for Skylo MAIN (cellular+satellite) + GPS u.FL antennas; LiPo 3.7 V → Mojo → +VBAT](diagrams/02-wiring-assembly.svg) -The whole stack — Notecarrier CX, Notecard for Skylo, accelerometer, haptic driver, and panic button — has to fit inside a belt-clip enclosure that a worker will forget they're wearing. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin headers; Notecard for Skylo seats into the M.2 slot and talks to the Cygnet host over the carrier's internal I²C. Because cellular, WiFi, and Skylo satellite all live on that one module, there is no companion satellite device to wire in — only the two u.FL antennas described below. The Mojo sits inline between the LiPo JST connector and the Notecarrier CX `+VBAT` pad during bench validation. +The whole stack — Notecarrier CX, Notecard for Skylo, accelerometer, haptic driver, and panic button — has to fit inside a belt-clip enclosure that a worker will forget they're wearing. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin headers; Notecard for Skylo seats into the M.2 slot and talks to the Cygnet host over the carrier's internal I²C. Because cellular, WiFi, and Skylo satellite all live on that one module, there is no companion satellite device to wire in — only the two u.FL antennas described below. The Mojo sits inline between the LiPo JST connector and the Notecarrier CX `+VBAT` pad during bench validation. All I²C peripherals (LIS3DH and DRV2605L) share the SDA/SCL bus exposed on the Notecarrier CX headers. On-board pull-ups are provided by the carrier; no external resistors are needed for the I²C lines. diff --git a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/README.md b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/README.md index b0665211..ace95399 100644 --- a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/README.md +++ b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/README.md @@ -133,13 +133,13 @@ The Adafruit MAX31865 (Product 3648) mounts inside the enclosure and connects to | Vin | +3V3_OUT | 3.3 V supply | | GND | GND | Common ground | | CLK | SCK | SPI clock | -| SDI | **MISO** (silkscreen label) | On Notecarrier CX v1.3 the MOSI and MISO silkscreen labels are swapped — the pin labeled **MISO** on the board is the actual master-out (MOSI) line. Connect MAX31865 SDI (data in to the chip) here. | +| SDI | **MISO** (silkscreen label) | On Notecarrier CX the MOSI and MISO silkscreen labels are swapped — the pin labeled **MISO** on the board is the actual master-out (MOSI) line. Connect MAX31865 SDI (data in to the chip) here. | | SDO | **MOSI** (silkscreen label) | The pin labeled **MOSI** on the board is the actual master-in (MISO) line. Connect MAX31865 SDO (data out from the chip) here. | | CS | D10 | Software chip-select | -**Notecarrier CX v1.3 label swap.** The MOSI and MISO pin labels are transposed on the CX v1.3 board silkscreen (see [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/)). The table above gives the correct physical connections. The Arduino STM32 SPI library drives the correct hardware-peripheral lines regardless of the silkscreen; the swap only affects how you run the jumper wires. +**Notecarrier CX label swap.** The MOSI and MISO pin labels are transposed on the CX board silkscreen (see [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/)). The table above gives the correct physical connections. The Arduino STM32 SPI library drives the correct hardware-peripheral lines regardless of the silkscreen; the swap only affects how you run the jumper wires. @@ -461,7 +461,7 @@ See the [MBGLW datasheet](https://dev.blues.io/datasheets/notecard-datasheet/not - Open the Notehub **Project → Devices** tab and look for the device serial number; if it appears in the device list but shows no Events, check the cellular signal at that location (weak signal can delay first registration). **Temperature readings are −9999 or missing.** -- The MAX31865 amplifier or PT1000 probe has a fault. Verify SPI wiring: CLK, SDI (MISO label on CX), SDO (MOSI label on CX), CS (D10), +3.3V, GND. Note the [Notecarrier CX v1.3 label swap](#rtd-temperature-amplifier-max31865) — the silkscreen labels MOSI and MISO are reversed; use the pin table in §5, not the labels. +- The MAX31865 amplifier or PT1000 probe has a fault. Verify SPI wiring: CLK, SDI (MISO label on CX), SDO (MOSI label on CX), CS (D10), +3.3V, GND. Note the [Notecarrier CX label swap](#rtd-temperature-amplifier-max31865) — the silkscreen labels MOSI and MISO are reversed; use the pin table in §5, not the labels. - If using a bench TMP117 instead, confirm the Qwiic cable is connected and that you have swapped the library from MAX31865 to SparkFun_TMP117 (see [§4](#4-hardware-requirements)). - Open the Notehub **Project → Terminal** tab, select your device, and run `card.status` to check if the Notecard is reporting a fault condition. diff --git a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.h b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.h index ebcbca24..ff29adc2 100644 --- a/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.h +++ b/61-regulatory-grade-pharmacy-lab-cold-storage-audit-monitor/firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.h @@ -29,31 +29,31 @@ #endif // Notefile names -#define NOTEFILE_READING "storage_reading.qo" -#define NOTEFILE_ALERT "storage_alert.qo" +#define NOTEFILE_READING "storage_reading.qo" +#define NOTEFILE_ALERT "storage_alert.qo" // Reed switch: Normally Open, terminal A → D5, terminal B → GND; INPUT_PULLUP. // LOW = door closed (magnet closes contacts); HIGH = door open. -#define DOOR_SWITCH_PIN D5 +#define DOOR_SWITCH_PIN D5 // MAX31865 SPI chip-select pin on the Notecarrier CX dual 16-pin header. // Uses hardware SPI (SCK/MOSI/MISO on the header); only CS is software-selectable. -// See §4 Wiring for the MOSI/MISO label-swap note specific to Notecarrier CX v1.3. -#define MAX31865_CS_PIN D10 +// See §4 Wiring for the MOSI/MISO label-swap note specific to Notecarrier CX. +#define MAX31865_CS_PIN D10 // Float-field sentinels written when the corresponding sensor has no valid data. // Downstream parsers must use the correct sentinel per field: // temp_c → -9999 (distinguishes MAX31865/probe failure from a near-zero reading) // lux → -1 (lux is always ≥ 0 in normal operation; -1 is unambiguously a fault) -#define SENTINEL_NO_DATA -9999.0f -#define SENTINEL_LUX_NO_DATA -1.0f +#define SENTINEL_NO_DATA -9999.0f +#define SENTINEL_LUX_NO_DATA -1.0f // Notecard NotePayload segment identifier -#define STATE_SEG_ID "STOR" +#define STATE_SEG_ID "STOR" // Capacity (including '\0') of the pending alert-type string. // "sensor_disagreement" is the longest type (19 chars); 24 provides headroom. -#define PENDING_ALERT_TYPE_LEN 24 +#define PENDING_ALERT_TYPE_LEN 24 // State magic + version guard. The high 16 bits (0xC5A0) are a fixed sentinel // (Cold Storage Audit); the low 16 bits are the schema version counter. @@ -64,7 +64,7 @@ // • note.template schema (fields or types) // A mismatch on restore forces full re-initialisation, clearing // notecard_configured and templates_defined so the updated config is applied. -#define STATE_MAGIC_VERSION 0xC5A00003UL +#define STATE_MAGIC_VERSION 0xC5A00003UL // Capacity of the persisted pending-note ring buffers (readings and alerts). // 4 slots covers four consecutive Notecard-unreachable wakes before the oldest @@ -73,48 +73,51 @@ // sent reading Note make any lost entries observable in Notehub. Increase // PENDING_RING_CAP if the deployment environment expects longer connectivity // gaps; the AppState struct (and therefore flash usage) grows proportionally. -#define PENDING_RING_CAP 4 +#define PENDING_RING_CAP 4 // Bitmask flags for tracking which alert types have an undelivered entry in the // pending_alerts ring. Used to suppress duplicate stashing while a send-failed // alert of the same type is already queued for retry. -#define ALERT_BIT_TEMP_HIGH 0x01u -#define ALERT_BIT_TEMP_LOW 0x02u -#define ALERT_BIT_DOOR_TIMEOUT 0x04u -#define ALERT_BIT_SENSOR_DISAGREE 0x08u +#define ALERT_BIT_TEMP_HIGH 0x01u +#define ALERT_BIT_TEMP_LOW 0x02u +#define ALERT_BIT_DOOR_TIMEOUT 0x04u +#define ALERT_BIT_SENSOR_DISAGREE 0x08u // --------------------------------------------------------------------------- // Pending-note element types // --------------------------------------------------------------------------- -struct PendingReading { - float temp_c; - float lux; - bool door_open; +struct PendingReading +{ + float temp_c; + float lux; + bool door_open; uint32_t door_open_sec; - bool time_valid; - uint32_t sample_epoch; // UTC epoch at sample time; 0 if time was not yet synced + bool time_valid; + uint32_t sample_epoch; // UTC epoch at sample time; 0 if time was not yet synced }; // A pending alert preserves the original event epoch so a retried Note can // include it in the body for audit lineage. note.add has no timestamp-override // field, so the Notecard stamps a retried Note with retry time in the envelope; // event_epoch carries the authoritative original trigger time. -struct PendingAlert { - char type[PENDING_ALERT_TYPE_LEN]; - float temp_c; - float lux; - bool door_open; +struct PendingAlert +{ + char type[PENDING_ALERT_TYPE_LEN]; + float temp_c; + float lux; + bool door_open; uint32_t door_open_sec; - bool time_valid; - uint32_t epoch; // UTC epoch when the alert condition was first detected + bool time_valid; + uint32_t epoch; // UTC epoch when the alert condition was first detected }; // --------------------------------------------------------------------------- // Application state — serialised into Notecard flash across sleep cycles via // NotePayloadSaveAndSleep / NotePayloadRetrieveAfterSleep. // --------------------------------------------------------------------------- -struct AppState { +struct AppState +{ // Must be first: validated immediately on restore. A mismatch (firmware // update, PRODUCT_UID change, schema change) triggers full re-init so // notecard_configured and templates_defined are reapplied. @@ -133,18 +136,18 @@ struct AppState { uint32_t last_door_timeout_alert_time; // Runtime configuration — overwritten from env vars on every wake. - float temp_high_c; - float temp_low_c; + float temp_high_c; + float temp_low_c; uint32_t door_alert_min; uint32_t alert_cooldown_sec; uint32_t sample_interval_sec; - float lux_threshold; + float lux_threshold; // Configuration flags — retried each wake until the Notecard confirms // each step without error. Both are cleared on magic_version mismatch so // config/schema updates are always applied after a firmware change. - bool notecard_configured; - bool templates_defined; + bool notecard_configured; + bool templates_defined; // Cumulative note.add failure counters. Included in every successfully // sent reading Note so losses are visible in Notehub; reset to 0 after @@ -158,16 +161,16 @@ struct AppState { // is incremented. Readings are buffered whenever a send fails or templates // are not yet confirmed; retries only proceed once templates_defined = true. PendingReading pending_reads[PENDING_RING_CAP]; - uint8_t pending_read_head; - uint8_t pending_read_count; + uint8_t pending_read_head; + uint8_t pending_read_count; // Pending alert ring buffer. Same FIFO semantics as the reading buffer. // Each entry preserves the original event epoch for body-field audit lineage. // Alert cooldown timestamps are NOT advanced on a failed send, so the alert // remains eligible without waiting through a full cooldown window. - PendingAlert pending_alerts[PENDING_RING_CAP]; - uint8_t pending_alert_head; - uint8_t pending_alert_count; + PendingAlert pending_alerts[PENDING_RING_CAP]; + uint8_t pending_alert_head; + uint8_t pending_alert_count; // Bitmask of ALERT_BIT_* flags. A set bit means at least one undelivered // entry of that alert type is currently in pending_alerts. The mask is @@ -176,27 +179,27 @@ struct AppState { // stale bits when an entry is overwritten on ring-full or dequeued without // being sent. Only entries actually present in the live ring contribute // a set bit. - uint8_t pending_alert_type_mask; + uint8_t pending_alert_type_mask; }; // --------------------------------------------------------------------------- // Globals defined in the .ino; accessible to helpers.cpp via these externs. // --------------------------------------------------------------------------- -extern Notecard notecard; -extern Adafruit_MAX31865 rtdAmp; -extern Adafruit_VEML7700 lightSensor; -extern AppState state; -extern bool tempSensorOk; -extern bool lightSensorOk; +extern Notecard notecard; +extern Adafruit_MAX31865 rtdAmp; +extern Adafruit_VEML7700 lightSensor; +extern AppState state; +extern bool tempSensorOk; +extern bool lightSensorOk; // --------------------------------------------------------------------------- // Prototypes for functions implemented in cold_storage_audit_monitor_helpers.cpp // --------------------------------------------------------------------------- -void fetchEnvOverrides(); +void fetchEnvOverrides(); -float readTemperatureC(); -float readLightLux(); -bool readDoorOpen(); +float readTemperatureC(); +float readLightLux(); +bool readDoorOpen(); uint32_t getEpochTime(); // Both return true when the Note was successfully enqueued by the Notecard, diff --git a/62-construction-site-environmental-noise-exposure-monitor/README.md b/62-construction-site-environmental-noise-exposure-monitor/README.md index 71981a37..0a4855c2 100644 --- a/62-construction-site-environmental-noise-exposure-monitor/README.md +++ b/62-construction-site-environmental-noise-exposure-monitor/README.md @@ -94,7 +94,7 @@ All Blues parts ship with an active SIM including 500 MB of data and 10 years of ![Wiring diagram: PMSA003I on Qwiic, SEN0232 on A0 and V+, power path from solar panel to Sunny Buddy MPPT with LiPo at BAT port, Sunny Buddy LOAD through Mojo to Notecarrier CX LiPo JST system input](diagrams/02-wiring-assembly.svg) -The whole enclosure has to survive being bolted to a fence post in the rain and operate on whatever sun the site gives it, so the build is small: a Notecarrier CX with the Notecard Cell+WiFi (MBGLW) in its M.2 slot, two sensors on opposite ends of the I²C and analog buses, and a solar power chain on the back. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin headers. The MBGLW exposes three u.FL connectors — **MAIN** (cellular), **GPS** (GNSS), and **WIFI**; MAIN and GPS each take an antenna in this design, and WIFI stays unconnected because cellular is the sole wireless path. See antenna routing below. +The whole enclosure has to survive being bolted to a fence post in the rain and operate on whatever sun the site gives it, so the build is small: a Notecarrier CX with the Notecard Cell+WiFi (MBGLW) in its M.2 slot, two sensors on opposite ends of the I²C and analog buses, and a solar power chain on the back. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin headers. The MBGLW exposes three u.FL connectors — **MAIN** (cellular), **GPS** (GNSS), and **WIFI**; MAIN and GPS each take an antenna in this design, and WIFI stays unconnected because cellular is the sole wireless path. See antenna routing below. **Power chain (solar → LiPo → Notecarrier):** diff --git a/63-construction-equipment-anti-theft-tracker-with-immobilizer/README.md b/63-construction-equipment-anti-theft-tracker-with-immobilizer/README.md index 9f397ae8..d8a199aa 100644 --- a/63-construction-equipment-anti-theft-tracker-with-immobilizer/README.md +++ b/63-construction-equipment-anti-theft-tracker-with-immobilizer/README.md @@ -28,7 +28,7 @@ This project is that device. It monitors GPS location against a configurable job ![System architecture: equipment signals → Notecarrier CX with Cygnet host and Notecard for Skylo → LTE-M/NB-IoT + Skylo satellite → Notehub → security and fleet-management routes](diagrams/01-system-architecture.svg) -**Device-side responsibilities.** The Cygnet STM32 host in the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) spends almost all of its time asleep — the whole power budget depends on it. When the configurable wake timer fires, the firmware reads ignition state from a voltage divider on the 12 V ignition line, polls the Notecard's built-in accelerometer via [`card.motion`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-motion), and pulls the most recent GPS fix via [`card.location`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location). It then runs three checks in order: is the position outside the Haversine-evaluated job-site geofence; is this the after-hours window; and is there a fresh immobilize command sitting in the inbound queue? Any heartbeat or alert Notes ride to the Notecard over I²C using the `note-arduino` request helpers, and the runtime state — geofence center, immobilizer stage, cadence parameters — is serialized to Notecard flash before sleep and rehydrated on the next wake via [`NotePayloadSaveAndSleep`](https://dev.blues.io/guides-and-tutorials/notecard-guides/attention-pin-guide/) / `NotePayloadRetrieveAfterSleep`. +**Device-side responsibilities.** The Cygnet STM32 host in the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) spends almost all of its time asleep — the whole power budget depends on it. When the configurable wake timer fires, the firmware reads ignition state from a voltage divider on the 12 V ignition line, polls the Notecard's built-in accelerometer via [`card.motion`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-motion), and pulls the most recent GPS fix via [`card.location`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-location). It then runs three checks in order: is the position outside the Haversine-evaluated job-site geofence; is this the after-hours window; and is there a fresh immobilize command sitting in the inbound queue? Any heartbeat or alert Notes ride to the Notecard over I²C using the `note-arduino` request helpers, and the runtime state — geofence center, immobilizer stage, cadence parameters — is serialized to Notecard flash before sleep and rehydrated on the next wake via [`NotePayloadSaveAndSleep`](https://dev.blues.io/guides-and-tutorials/notecard-guides/attention-pin-guide/) / `NotePayloadRetrieveAfterSleep`. **Notecard responsibilities.** Notecard for Skylo is doing most of the connectivity work behind the scenes. It buffers queued [Notes](https://dev.blues.io/api-reference/glossary/#note) in on-device flash, opens a cellular or satellite session on the [`hub.set`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-set) `outbound` cadence, and treats any `sync:true` alert Note as an immediate uplink — the difference between an alert reaching the operator in seconds and the equipment crossing a county line. The same module also owns the GPS receiver, the accelerometer, the real-time clock, and the battery-voltage ADC, so no external sensors are needed. [Environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables/) flow back from Notehub on each inbound sync — a fleet manager can retune the geofence radius or the after-hours window from a browser, and the firmware automatically reissues `hub.set` and `card.location.mode` so all three Notecard cadences stay aligned with the new wake interval. @@ -113,7 +113,7 @@ 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) ([datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/)) | 1 | Compact carrier with embedded Cygnet STM32 host MCU — no separate Swan or Feather needed. ATTN pin is wired to control the Cygnet's power rail, enabling deep sleep via `card.attn`. | +| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/)) | 1 | Compact carrier with embedded Cygnet STM32 host MCU — no separate Swan or Feather needed. ATTN pin is wired to control the Cygnet's power rail, enabling deep sleep via `card.attn`. | | [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/)) | 1 | All-in-one LTE-M/NB-IoT/GPRS + WiFi + Skylo satellite in a single M.2 module. Automatic cellular-to-satellite fallover with no firmware changes. Ships with its Skylo-certified `MAIN` cellular/satellite antenna (see antenna row below). Includes 500 MB cellular data and 10 KB/month Skylo data. | | [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) ([datasheet](https://dev.blues.io/datasheets/mojo-datasheet/)) | 1 | Bench-only coulomb counter for energy validation during commissioning. Placed inline on the +VBAT rail to measure current draw across sleep, active, and transmit phases. Not deployed in the field (see §11). | | 3.7 V LiPo battery, 2000 mAh, JST-PH 2.0 mm (e.g. [Adafruit 2011](https://www.adafruit.com/product/2011), or equivalent 2000–4000 mAh cell) | 1 | Primary energy storage. Size capacity against expected solar availability and desired dark-sky reserve; see §9 for a phase-by-phase current-draw breakdown to inform your sizing. | diff --git a/65-off-grid-livestock-water-tank-monitor/README.md b/65-off-grid-livestock-water-tank-monitor/README.md index 04380bd9..1754a25d 100644 --- a/65-off-grid-livestock-water-tank-monitor/README.md +++ b/65-off-grid-livestock-water-tank-monitor/README.md @@ -108,7 +108,7 @@ All Blues hardware ships with an active SIM including 500 MB of cellular data an ![Wiring: MB7389 ultrasonic to A0; SCT-013 CT with V/2 bias to A1; battery divider to A2 with PMOS gated by A3; Skylo MAIN antenna via u.FL to SMA bulkhead; solar 20 W → charge controller → 12 V battery → 5 V step-down → +VBAT](diagrams/02-wiring-assembly.svg) -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. Notecard for Skylo seats into the M.2 slot. The Mojo is a bench power-measurement instrument spliced inline between the step-down regulator output and the Notecarrier's `+VBAT` pad during commissioning; it is not read by the deployed firmware and is removed before field installation. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. Notecard for Skylo seats into the M.2 slot. The Mojo is a bench power-measurement instrument spliced inline between the step-down regulator output and the Notecarrier's `+VBAT` pad during commissioning; it is not read by the deployed firmware and is removed before field installation. @@ -295,7 +295,7 @@ Both Notefiles are [template-backed](https://dev.blues.io/notecard/notecard-walk ### Low-power strategy -The Cygnet is awake for only a few seconds per 15-minute sample cycle. After each wake, `NotePayloadSaveAndSleep` serializes the runtime state (accumulator values, alert cooldown timestamps, summary window counter, and last-applied hub cadence) into Notecard flash, then triggers [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to assert the ATTN pin, which on the Notecarrier CX is routed to the board's `EN` input. Per the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/), `EN` "gates the board's host 3.3V rail," cutting the Cygnet host power entirely. The `+3V3_OUT` header pin is sourced from that same regulated rail, so sensors wired to it — the MB7389 level sensor and the CT bias network — are also unpowered for the full sleep interval. The battery-voltage measurement circuit is similarly off: the BSS84 PMOS gate is held at 12 V by the 100 kΩ pullup (independent of MCU power), leaving A2 at GND through the low-side 10 kΩ and drawing zero quiescent current from the battery bus. The NOTE-NBGLWX Notecard itself idles at approximately 18 µA @ 5V in its own [low-power idle state](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) between sessions, with the Skylo satellite modem also in low-power standby when not transmitting. +The Cygnet is awake for only a few seconds per 15-minute sample cycle. After each wake, `NotePayloadSaveAndSleep` serializes the runtime state (accumulator values, alert cooldown timestamps, summary window counter, and last-applied hub cadence) into Notecard flash, then triggers [`card.attn`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) to assert the ATTN pin, which on the Notecarrier CX is routed to the board's `EN` input. Per the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/), `EN` "gates the board's host 3.3V rail," cutting the Cygnet host power entirely. The `+3V3_OUT` header pin is sourced from that same regulated rail, so sensors wired to it — the MB7389 level sensor and the CT bias network — are also unpowered for the full sleep interval. The battery-voltage measurement circuit is similarly off: the BSS84 PMOS gate is held at 12 V by the 100 kΩ pullup (independent of MCU power), leaving A2 at GND through the low-side 10 kΩ and drawing zero quiescent current from the battery bus. The NOTE-NBGLWX Notecard itself idles at approximately 18 µA @ 5V in its own [low-power idle state](https://dev.blues.io/notecard/notecard-walkthrough/low-power-firmware-design/) between sessions, with the Skylo satellite modem also in low-power standby when not transmitting. Sampling and transmission are deliberately decoupled: the device samples every 15 minutes but transmits summaries only every 4 hours. Alerts bypass the transmit timer via `sync:true` — a critically low tank reaches the rancher's phone within a session-establishment window regardless of where the Notecard is in its outbound cycle. Over cellular, that window is typically 15–60 seconds. Over the Skylo NTN satellite link, session establishment takes longer — the Notecard must acquire the GEO satellite and complete the NTN registration before data flows, which can take a few minutes depending on satellite geometry and signal conditions. Alert Notes are still delivered via satellite when cellular is unavailable; the delivery latency is higher than over cellular, but the consequence of a missed alert on an off-grid tank with no other connectivity path is worse than a slightly delayed one. For sites with reliable cellular, the cellular path is used first and the satellite path is never exercised; satellite energy cost does not apply. diff --git a/66-remote-apiary-hive-health-monitor/README.md b/66-remote-apiary-hive-health-monitor/README.md index 99357c9f..162f2c7a 100644 --- a/66-remote-apiary-hive-health-monitor/README.md +++ b/66-remote-apiary-hive-health-monitor/README.md @@ -106,7 +106,7 @@ Here is a sample Note this device emits: | Part | Qty | Rationale | |------|-----|-----------| -| [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) ([buy](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host MCU — no separate Swan needed. Provides A0–A5 analog inputs, SDA/SCL I2C, and digital GPIO pins sufficient for all three sensors. | +| [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) ([buy](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)) | 1 | Integrated carrier with an embedded Cygnet STM32L433 host MCU — no separate Swan needed. Provides A0–A5 analog inputs, SDA/SCL I2C, and digital GPIO pins sufficient for all three sensors. | | [Notecard for Skylo (NOTE-NBGLWX)](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/) ([buy](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)) | 1 | One M.2 module carrying cellular (LTE-M / NB-IoT / GPRS, Quectel BG95-S5 modem), WiFi (Silicon Labs WFM200S), and Skylo satellite (NTN) radios. Seats into the Notecarrier CX M.2 slot. The firmware's `card.transport` `wifi-cell-ntn` setting makes it prefer cellular at in-coverage apiaries and fall back automatically to the Skylo satellite network at yards beyond cellular reach — no second device or part-number decision, and no separate satellite enclosure. The integrated prepaid global SIM means no carrier contract and no per-site IT negotiation. Skylo GEO coverage varies by geography and service region; verify coverage for your deployment area before relying on satellite as the sole backhaul (see [satellite best practices](https://dev.blues.io/starnote/satellite-best-practices/)). This design does not configure or transmit device location — the board's GPS/GNSS receiver is used by the satellite stack internally for timing and ephemeris, not as a user-facing location source. Requires the antennas below. | | M16 cable glands (nylon, IP68) | 2 | One for the sensor cable bundle and one for the antenna pigtail penetration in the main enclosure. | | [Blues Mojo](https://dev.blues.io/datasheets/mojo-datasheet/) ([buy](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link)) | 1 | **Bench commissioning only.** Coulomb counter for power-budget validation. Mounts inline on the Sunny Buddy load rail between the charger and the Notecarrier CX `+VBAT` pad; wired to Notecarrier CX Qwiic during bench testing. Must be removed from the field assembly before deployment. See §9 for usage. | @@ -130,7 +130,7 @@ Here is a sample Note this device emits: ![Wiring diagram: HX711 on D5/D6, SHT31-D via 4-wire jumpers on SDA/SCL, MAX9814 on A0, Skylo-certified antenna on MAIN u.FL plus GPS antenna, power chain: solar → Sunny Buddy (Li-ion at BATT) → LOAD → Mojo → Notecarrier +VBAT](diagrams/02-wiring-assembly.svg) -The enclosure mounts on the hive stand and the bees never know it's there — three sensor leads thread through existing seams, the antenna lives outside on the lid, and a small solar panel feeds the whole thing from a gooseneck bracket nearby. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin headers, and Notecard for Skylo seats into the M.2 slot. Its `MAIN` u.FL port connects to the included Skylo-certified antenna — which carries both the cellular and satellite signals — and its `GPS` u.FL port connects to the passive GPS/GNSS antenna; both mount outside the box with an unobstructed view of the sky. +The enclosure mounts on the hive stand and the bees never know it's there — three sensor leads thread through existing seams, the antenna lives outside on the lid, and a small solar panel feeds the whole thing from a gooseneck bracket nearby. Every host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin headers, and Notecard for Skylo seats into the M.2 slot. Its `MAIN` u.FL port connects to the included Skylo-certified antenna — which carries both the cellular and satellite signals — and its `GPS` u.FL port connects to the passive GPS/GNSS antenna; both mount outside the box with an unobstructed view of the sky. **Power chain.** The Sunny Buddy MPPT charger sits between the solar panel and the Li-ion battery. The Li-ion pack connects to the Sunny Buddy's `BATT` JST connector (or `BAT+`/`GND` screw terminals). The system load is drawn from the Sunny Buddy's `LOAD+`/`GND` screw terminals; the Notecarrier CX `+VBAT` pad connects to `LOAD+` and `GND` connects to the common ground rail. During bench commissioning, Mojo sits inline on the positive load rail to measure downstream device current: 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 376f8f5d..64bfd7ce 100644 --- a/69-injection-molding-shot-to-shot-process-monitor/README.md +++ b/69-injection-molding-shot-to-shot-process-monitor/README.md @@ -124,7 +124,7 @@ All Blues parts ship with an active SIM including 500 MB of data and 10 years of ![Wiring: 4–20 mA pressure transducer to A0 (with 150 Ω shunt); MAX31855K thermocouple on SPI with CS=D10; external SMA antenna via u.FL pigtail; 24 VDC → SD-25B-5 → Mojo → +VBAT](diagrams/02-wiring-assembly.svg) -Inside the DIN-rail enclosure everything funnels back to the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) 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. @@ -152,7 +152,7 @@ Inside the DIN-rail enclosure everything funnels back to the [Notecarrier CX](ht - MAX31855K breakout `SCK` → `SCK` on the Notecarrier CX header. - MAX31855K breakout `DO` (MISO) → `MISO` on the Notecarrier CX header. - > **Notecarrier CX v1.3 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 v1.3 datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) for the authoritative pin table. + > **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. diff --git a/70-trailer-manufacturer-connected-trailer-platform/README.md b/70-trailer-manufacturer-connected-trailer-platform/README.md index c3117e8d..680dbf2a 100644 --- a/70-trailer-manufacturer-connected-trailer-platform/README.md +++ b/70-trailer-manufacturer-connected-trailer-platform/README.md @@ -160,7 +160,7 @@ The following items are **not part of this reference build** and should not be p ![Wiring: 2 NTC thermistors to A0/A1, rear-door reed to D9 INPUT_PULLUP, MAIN antenna to u.FL (cell + Skylo sat), passive GPS antenna to GPS u.FL; dual 12 V input through OR-circuit and transient protection → DC-DC 5 V → +VBAT](diagrams/02-wiring-assembly.svg) -Inside the nose-wall enclosure, everything traces back to the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) and its dual 16-pin headers. Notecard for Skylo seats into the carrier's M.2 slot, and the rest of the trailer-side wiring — thermistor leads, door reed, J560-pin-6 supply, antenna pigtails — comes back to that header from the rest of the box. Two routing rules are worth calling out upfront because they're easy to get wrong: the GPS antenna coax exits the enclosure through the weatherproof u.FL-to-SMA bulkhead feedthrough, while the Skylo MAIN antenna lead must pass through an IP68 cable gland only — never through a bulkhead adapter or any additional RF connector (see BOM and the MAIN u.FL bullet below). The Mojo sits inline between the DC-DC converter's 5V output and the Notecarrier's +VBAT pad during bench power validation (remove it from the field unit once commissioning is complete, or leave it in place if fleet-level energy telemetry is desired). In field installations the DC-DC converter's VIN is fed from the power-priority switching module described in the power chain below, not directly from J560 pin 6. +Inside the nose-wall enclosure, everything traces back to the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) and its dual 16-pin headers. Notecard for Skylo seats into the carrier's M.2 slot, and the rest of the trailer-side wiring — thermistor leads, door reed, J560-pin-6 supply, antenna pigtails — comes back to that header from the rest of the box. Two routing rules are worth calling out upfront because they're easy to get wrong: the GPS antenna coax exits the enclosure through the weatherproof u.FL-to-SMA bulkhead feedthrough, while the Skylo MAIN antenna lead must pass through an IP68 cable gland only — never through a bulkhead adapter or any additional RF connector (see BOM and the MAIN u.FL bullet below). The Mojo sits inline between the DC-DC converter's 5V output and the Notecarrier's +VBAT pad during bench power validation (remove it from the field unit once commissioning is complete, or leave it in place if fleet-level energy telemetry is desired). In field installations the DC-DC converter's VIN is fed from the power-priority switching module described in the power chain below, not directly from J560 pin 6. Pin-by-pin: diff --git a/72-solar-array-string-level-performance-dashboard/README.md b/72-solar-array-string-level-performance-dashboard/README.md index 9a04e6f7..8e36beb6 100644 --- a/72-solar-array-string-level-performance-dashboard/README.md +++ b/72-solar-array-string-level-performance-dashboard/README.md @@ -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-3/) 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):** diff --git a/74-returnable-container-tote-pool-tracker/README.md b/74-returnable-container-tote-pool-tracker/README.md index 5740df18..697f18d3 100644 --- a/74-returnable-container-tote-pool-tracker/README.md +++ b/74-returnable-container-tote-pool-tracker/README.md @@ -434,7 +434,7 @@ Multi-year service life — 3 to 5+ years between swaps — requires a Li-SOCl Useful Mojo bench validation: leave the assembly running for 24 h and confirm the Mojo tally is in the 1–5 mAh range. Deviations larger than ~5× typically indicate one of: -- **Host never sleeping:** flat 10–80 mA continuous baseline. Most common cause: ATTN power-gating is not active, or `NotePayloadSaveAndSleep` is returning early. On the Notecarrier CX the Notecard's ATTN pin is routed internally to the `EN` input (described as "Input that gates the board's host 3.3V rail" in the [Notecarrier CX v1.3 header table](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/)) — this is a fixed PCB trace, not a user jumper. If you are not seeing host power-gating, confirm you are using a Notecarrier CX specifically (not a different carrier board), and that no external signal is holding `EN` high. +- **Host never sleeping:** flat 10–80 mA continuous baseline. Most common cause: ATTN power-gating is not active, or `NotePayloadSaveAndSleep` is returning early. On the Notecarrier CX the Notecard's ATTN pin is routed internally to the `EN` input (described as "Input that gates the board's host 3.3V rail" in the [Notecarrier CX header table](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/)) — this is a fixed PCB trace, not a user jumper. If you are not seeing host power-gating, confirm you are using a Notecarrier CX specifically (not a different carrier board), and that no external signal is holding `EN` high. - **Excessive cellular retries:** sync bursts are longer than expected or firing more than once per day. Check signal strength via [`card.wireless`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-wireless) or [`hub.status`](https://dev.blues.io/api-reference/notecard-api/hub-requests/#hub-status) in the blues.dev In-Browser Terminal. - **Motion false triggers:** `tote_event.qo` Notes are accumulating from dock-floor vibration or transport. Raise `motion_threshold` via the Fleet environment variable and watch the event rate drop without reflashing. @@ -460,7 +460,7 @@ Useful Mojo bench validation: leave the assembly running for 24 h and confirm th **Battery drains much faster than expected (>5 mAh/day consumed).** -- Confirm the ATTN pin power-gating is active: use the Mojo coulomb counter or measure the Cygnet current rail directly. If the host is drawing continuous 10–80 mA, the ATTN power-gating is not functioning. On a Notecarrier CX the ATTN pin is internally routed to the `EN` input (confirmed in the board's [v1.3 datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/)). If adapting this design to a different carrier, verify the ATTN-to-host-power wiring (see the [Attention Pin Guide](https://dev.blues.io/guides-and-tutorials/notecard-guides/attention-pin-guide/)). +- Confirm the ATTN pin power-gating is active: use the Mojo coulomb counter or measure the Cygnet current rail directly. If the host is drawing continuous 10–80 mA, the ATTN power-gating is not functioning. On a Notecarrier CX the ATTN pin is internally routed to the `EN` input (confirmed in the board's [v1.3 datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/)). If adapting this design to a different carrier, verify the ATTN-to-host-power wiring (see the [Attention Pin Guide](https://dev.blues.io/guides-and-tutorials/notecard-guides/attention-pin-guide/)). - Check signal strength. Weak-signal retries add extra cellular sessions and burn more energy. Use [`card.wireless`](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-wireless) in the blues.dev In-Browser Terminal to inspect RSSI and bars. - Motion events are accumulating more than once per day. Raise `motion_threshold` and increase `motion_bucket_sec` to reduce false triggers. 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 1f5fdf58..7bdc5726 100644 --- a/75-heavy-equipment-hours-of-use-utilization-tracker/README.md +++ b/75-heavy-equipment-hours-of-use-utilization-tracker/README.md @@ -90,7 +90,7 @@ Pin-by-pin: - **SCL** → LSM6DSOX `SCL` - **Solar JST** → 6V solar panel (observe polarity marked on connector; red = positive) - **LiPo JST** → 3.7V 2000 mAh LiPo cell (JST-PH 2-pin, same polarity convention) -- **ATTN → EN jumper (REQUIRED for host power-gating).** On Notecarrier CX v1.3, `ATTN` (the Notecard's configurable interrupt output) and `EN` (the input that gates the carrier's host 3.3 V rail to the Cygnet) are exposed as **separate** pins on the dual 16-pin header. See the [Notecarrier CX v1.3 datasheet header description](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/#notecarrier-cx-dual-16-pin-headers). They are **not** connected internally. To make `card.attn mode:sleep` actually cut power to the Cygnet, run a short jumper wire from the `ATTN` header pin to the `EN` header pin. With that wire in place, when `goToSleep()` issues `card.attn mode:sleep`, the Notecard drives `ATTN` low → `EN` low → the host 3.3 V rail collapses and the Cygnet powers off. `SAMPLE_INTERVAL_SEC` later the Notecard reasserts `ATTN` high → `EN` high → the Cygnet powers up and re-enters `setup()`. Without the `ATTN→EN` jumper the host runs continuously between samples; the firmware's `loop()` fall-back will keep functional sampling working, but baseline current will be ~5–10 mA instead of the ~20–60 µA target, and the solar/battery budget in §10 will not hold. Verify the jumper is functioning by confirming the Cygnet's serial output stops during the sleep window. See the [`card.attn` API reference](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) and the [Feather MCU low-power guide](https://dev.blues.io/guides-and-tutorials/notecard-guides/feather-mcu-low-power-management/) for additional context on this pattern. +- **ATTN → EN jumper (REQUIRED for host power-gating).** On Notecarrier CX, `ATTN` (the Notecard's configurable interrupt output) and `EN` (the input that gates the carrier's host 3.3 V rail to the Cygnet) are exposed as **separate** pins on the dual 16-pin header. See the [Notecarrier CX datasheet header description](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/#notecarrier-cx-dual-16-pin-headers). They are **not** connected internally. To make `card.attn mode:sleep` actually cut power to the Cygnet, run a short jumper wire from the `ATTN` header pin to the `EN` header pin. With that wire in place, when `goToSleep()` issues `card.attn mode:sleep`, the Notecard drives `ATTN` low → `EN` low → the host 3.3 V rail collapses and the Cygnet powers off. `SAMPLE_INTERVAL_SEC` later the Notecard reasserts `ATTN` high → `EN` high → the Cygnet powers up and re-enters `setup()`. Without the `ATTN→EN` jumper the host runs continuously between samples; the firmware's `loop()` fall-back will keep functional sampling working, but baseline current will be ~5–10 mA instead of the ~20–60 µA target, and the solar/battery budget in §10 will not hold. Verify the jumper is functioning by confirming the Cygnet's serial output stops during the sleep window. See the [`card.attn` API reference](https://dev.blues.io/api-reference/notecard-api/card-requests/#card-attn) and the [Feather MCU low-power guide](https://dev.blues.io/guides-and-tutorials/notecard-guides/feather-mcu-low-power-management/) for additional context on this pattern. **Antenna placement.** The NOTE-NBGLWX has exactly two u.FL antenna ports: diff --git a/75-heavy-equipment-hours-of-use-utilization-tracker/firmware/equipment_hours_tracker/equipment_hours_tracker_helpers.cpp b/75-heavy-equipment-hours-of-use-utilization-tracker/firmware/equipment_hours_tracker/equipment_hours_tracker_helpers.cpp index 78c4ed4e..ab0790f3 100644 --- a/75-heavy-equipment-hours-of-use-utilization-tracker/firmware/equipment_hours_tracker/equipment_hours_tracker_helpers.cpp +++ b/75-heavy-equipment-hours-of-use-utilization-tracker/firmware/equipment_hours_tracker/equipment_hours_tracker_helpers.cpp @@ -9,21 +9,21 @@ #include "equipment_hours_tracker_helpers.h" // ── Global definitions ──────────────────────────────────────────────────────── -Notecard notecard; +Notecard notecard; Adafruit_LSM6DSOX sox; PersistState g_s; -const char SEG_ID[] = "EQHRS"; +const char SEG_ID[] = "EQHRS"; // Runtime env overrides — static initialisers provide the compile-time defaults, // but fetchEnvOverrides() seeds these from g_s.applied_* on every wake before // calling env.get, so a transient miss never reverts a previously-applied value. -float g_vib_run_mg = VIB_RUN_MG_DEFAULT; -float g_vib_cv_max = VIB_CV_MAX_DEFAULT; +float g_vib_run_mg = VIB_RUN_MG_DEFAULT; +float g_vib_cv_max = VIB_CV_MAX_DEFAULT; uint32_t g_summary_interval_min = SUMMARY_INTERVAL_MIN; -float g_fence_lat = 0.0f; -float g_fence_lon = 0.0f; -uint32_t g_fence_radius_m = 0; +float g_fence_lat = 0.0f; +float g_fence_lon = 0.0f; +uint32_t g_fence_radius_m = 0; // ── Tri-state env result ────────────────────────────────────────────────────── // ENV_OK — a non-empty text field was returned; parsed value written to *out. @@ -36,7 +36,12 @@ uint32_t g_fence_radius_m = 0; // Collapsing ENV_UNSET and ENV_ERROR into one 'false' path (the prior design) // made thresholds, geofence parameters, and summary cadence sticky indefinitely: // deleting a Notehub variable could never revert a setting to its default. -typedef enum : int8_t { ENV_ERROR = -1, ENV_UNSET = 0, ENV_OK = 1 } EnvResult; +typedef enum : int8_t +{ + ENV_ERROR = -1, + ENV_UNSET = 0, + ENV_OK = 1 +} EnvResult; // ── File-local env-read helper ──────────────────────────────────────────────── // Issues a single env.get request. Frees the response unconditionally; writes @@ -46,19 +51,26 @@ typedef enum : int8_t { ENV_ERROR = -1, ENV_UNSET = 0, ENV_OK = 1 } EnvResult; // atof(v) MUST be called before deleteResponse(rsp) frees that heap; calling // atof on a dangling pointer after the free is undefined behaviour that can // produce intermittent garbage values and make env overrides appear flaky. -static EnvResult envReadDouble(const char *name, double &out) { +static EnvResult envReadDouble(const char *name, double &out) +{ J *req = notecard.newRequest("env.get"); JAddStringToObject(req, "name", name); J *rsp = notecard.requestAndResponse(req); - if (!rsp) return ENV_ERROR; + if (!rsp) + return ENV_ERROR; const char *err = JGetString(rsp, "err"); bool has_err = (err && *err != '\0'); const char *v = JGetString(rsp, "text"); - bool has_val = (v && *v != '\0'); - double parsed = has_val ? atof(v) : 0.0; // parse before freeing the JSON object - notecard.deleteResponse(rsp); // v is now invalid — do not use after this line - if (has_err) return ENV_ERROR; - if (has_val) { out = parsed; return ENV_OK; } + bool has_val = (v && *v != '\0'); + double parsed = has_val ? atof(v) : 0.0; // parse before freeing the JSON object + notecard.deleteResponse(rsp); // v is now invalid — do not use after this line + if (has_err) + return ENV_ERROR; + if (has_val) + { + out = parsed; + return ENV_OK; + } return ENV_UNSET; } @@ -66,12 +78,18 @@ static EnvResult envReadDouble(const char *name, double &out) { // Sends a pre-built request and returns true only if the Notecard replies // without an "err" field. Frees the response. Used for all critical // configuration and data-path requests where a silent failure is unacceptable. -bool checkedRequest(J *req) { +bool checkedRequest(J *req) +{ J *rsp = notecard.requestAndResponse(req); - if (!rsp) return false; + if (!rsp) + return false; const char *err = JGetString(rsp, "err"); bool ok = (!err || *err == '\0'); - if (!ok) { Serial.print("[ERR] "); Serial.println(err); } + if (!ok) + { + Serial.print("[ERR] "); + Serial.println(err); + } notecard.deleteResponse(rsp); return ok; } @@ -79,12 +97,16 @@ bool checkedRequest(J *req) { // ── Env-var clamping helpers ────────────────────────────────────────────────── // A bad fleet-level env var must never make the classifier unusable or drive an // integer outside a safe operating range; return fallback when out of bounds. -float clampF(double v, float minv, float maxv, float fallback) { - if (v < (double)minv || v > (double)maxv) return fallback; +float clampF(double v, float minv, float maxv, float fallback) +{ + if (v < (double)minv || v > (double)maxv) + return fallback; return (float)v; } -uint32_t clampU32(long v, uint32_t minv, uint32_t maxv, uint32_t fallback) { - if (v < (long)minv || v > (long)maxv) return fallback; +uint32_t clampU32(long v, uint32_t minv, uint32_t maxv, uint32_t fallback) +{ + if (v < (long)minv || v > (long)maxv) + return fallback; return (uint32_t)v; } @@ -92,10 +114,12 @@ uint32_t clampU32(long v, uint32_t minv, uint32_t maxv, uint32_t fallback) { // Returns true only after every request is confirmed by the Notecard so that // g_s.configured is not set when a transient I²C failure leaves configuration // incomplete. -bool notecardConfigure(void) { +bool notecardConfigure(void) +{ // Runtime guard: an empty PRODUCT_UID is a first-light misconfiguration. // A compile-time #pragma message is easy to miss; a loud serial error is not. - if (*PRODUCT_UID == '\0') { + if (*PRODUCT_UID == '\0') + { Serial.println("[CFG] PRODUCT_UID is empty — set it to your Notehub ProductUID before flashing"); return false; } @@ -105,11 +129,12 @@ bool notecardConfigure(void) { // value is checked so that a failed hub.set causes a retry on the next wake // rather than leaving the device with an invalid project association. J *req = notecard.newRequest("hub.set"); - JAddStringToObject(req, "product", PRODUCT_UID); - JAddStringToObject(req, "mode", "periodic"); + JAddStringToObject(req, "product", PRODUCT_UID); + JAddStringToObject(req, "mode", "periodic"); JAddNumberToObject(req, "outbound", SUMMARY_INTERVAL_MIN); // daily outbound - JAddNumberToObject(req, "inbound", 480); // 8-hour env-var pull - if (!notecard.sendRequestWithRetry(req, 10)) { + JAddNumberToObject(req, "inbound", 480); // 8-hour env-var pull + if (!notecard.sendRequestWithRetry(req, 10)) + { Serial.println("[CFG] hub.set failed"); return false; } @@ -126,21 +151,24 @@ bool notecardConfigure(void) { // persists in the Notecard's own flash, so issuing it once at cold boot suffices. req = notecard.newRequest("card.transport"); JAddStringToObject(req, "method", "wifi-cell-ntn"); - if (!checkedRequest(req)) return false; + if (!checkedRequest(req)) + return false; // Periodic GPS: acquire a fix every GPS_PERIOD_SECONDS (15 min). req = notecard.newRequest("card.location.mode"); - JAddStringToObject(req, "mode", "periodic"); + JAddStringToObject(req, "mode", "periodic"); JAddNumberToObject(req, "seconds", GPS_PERIOD_SECONDS); - if (!checkedRequest(req)) return false; + if (!checkedRequest(req)) + return false; // Heartbeat tracking: emit _track.qo every GPS_HEARTBEAT_HOURS even when // stationary so the asset remains visible on the map. req = notecard.newRequest("card.location.track"); - JAddBoolToObject(req, "start", true); + JAddBoolToObject(req, "start", true); JAddBoolToObject(req, "heartbeat", true); - JAddNumberToObject(req, "hours", GPS_HEARTBEAT_HOURS); - if (!checkedRequest(req)) return false; + JAddNumberToObject(req, "hours", GPS_HEARTBEAT_HOURS); + if (!checkedRequest(req)) + return false; // Optionally disable the Notecard's internal accelerometer to avoid // interference with the external LSM6DSOX and save ~0.5 mA. @@ -152,7 +180,8 @@ bool notecardConfigure(void) { #ifdef DISABLE_NOTECARD_MOTION req = notecard.newRequest("card.motion.mode"); JAddBoolToObject(req, "stop", true); - if (!checkedRequest(req)) return false; + if (!checkedRequest(req)) + return false; #endif return true; @@ -163,22 +192,24 @@ bool notecardConfigure(void) { // size ~3–5× vs. free-form JSON. Satellite packets are limited to 256 bytes. // _lat / _lon are compact-template keywords auto-filled by the Notecard GPS. // Returns true only after both templates are confirmed by the Notecard. -bool defineTemplates(void) { +bool defineTemplates(void) +{ // equip_summary.qo — daily engine-hours summary // 14.1 = 4-byte IEEE-754 float; 12.1 = 2-byte float; 12 = 2-byte integer J *req = notecard.newRequest("note.template"); - JAddStringToObject(req, "file", "equip_summary.qo"); - JAddNumberToObject(req, "port", 50); + JAddStringToObject(req, "file", "equip_summary.qo"); + JAddNumberToObject(req, "port", 50); JAddStringToObject(req, "format", "compact"); J *body = JAddObjectToObject(req, "body"); - JAddNumberToObject(body, "run_h", 14.1); + JAddNumberToObject(body, "run_h", 14.1); JAddNumberToObject(body, "run_h_total", 14.1); JAddNumberToObject(body, "transport_h", 14.1); - JAddNumberToObject(body, "bat_v", 12.1); - JAddNumberToObject(body, "fault_ct", 12); // event-queue overflow counter (2-byte int) - JAddNumberToObject(body, "_lat", 14.1); - JAddNumberToObject(body, "_lon", 14.1); - if (!checkedRequest(req)) return false; + JAddNumberToObject(body, "bat_v", 12.1); + JAddNumberToObject(body, "fault_ct", 12); // event-queue overflow counter (2-byte int) + JAddNumberToObject(body, "_lat", 14.1); + JAddNumberToObject(body, "_lon", 14.1); + if (!checkedRequest(req)) + return false; // equip_event.qo — state-change events; hub.sync is issued after each // successful note.add to request prompt delivery. @@ -187,17 +218,18 @@ bool defineTemplates(void) { // "transport_start" (15 chars) is the longest of the four event names. // 14.1 = 4-byte IEEE-754 float; 14 = 4-byte signed integer (fits Unix epoch). req = notecard.newRequest("note.template"); - JAddStringToObject(req, "file", "equip_event.qo"); - JAddNumberToObject(req, "port", 51); + JAddStringToObject(req, "file", "equip_event.qo"); + JAddNumberToObject(req, "port", 51); JAddStringToObject(req, "format", "compact"); body = JAddObjectToObject(req, "body"); - JAddStringToObject(body, "event", "transport_start"); // longest event name + JAddStringToObject(body, "event", "transport_start"); // longest event name JAddNumberToObject(body, "session_min", 14.1); JAddNumberToObject(body, "run_h_total", 14.1); - JAddNumberToObject(body, "epoch", 14); // 4-byte int: Unix transition timestamp (s) - JAddNumberToObject(body, "_lat", 14.1); - JAddNumberToObject(body, "_lon", 14.1); - if (!checkedRequest(req)) return false; + JAddNumberToObject(body, "epoch", 14); // 4-byte int: Unix transition timestamp (s) + JAddNumberToObject(body, "_lat", 14.1); + JAddNumberToObject(body, "_lon", 14.1); + if (!checkedRequest(req)) + return false; return true; } @@ -214,29 +246,36 @@ bool defineTemplates(void) { // Step 2 — Issue env.get for each tunable. A successful read with a valid // value overwrites the seed AND updates g_s.applied_* so the next wake // inherits it. A failed or empty read leaves the seeded value unchanged. -void fetchEnvOverrides(void) { +void fetchEnvOverrides(void) +{ // ── Step 1: seed from last-good env reads ───────────────────────────────── g_vib_run_mg = (g_s.applied_vib_run_mg >= 1.0f && g_s.applied_vib_run_mg <= 500.0f) - ? g_s.applied_vib_run_mg : VIB_RUN_MG_DEFAULT; + ? g_s.applied_vib_run_mg + : VIB_RUN_MG_DEFAULT; g_vib_cv_max = (g_s.applied_vib_cv_max >= 0.05f && g_s.applied_vib_cv_max <= 2.0f) - ? g_s.applied_vib_cv_max : VIB_CV_MAX_DEFAULT; + ? g_s.applied_vib_cv_max + : VIB_CV_MAX_DEFAULT; g_summary_interval_min = (g_s.applied_summary_interval_min >= 60 && - g_s.applied_summary_interval_min <= 44640) - ? g_s.applied_summary_interval_min : SUMMARY_INTERVAL_MIN; + g_s.applied_summary_interval_min <= 44640) + ? g_s.applied_summary_interval_min + : SUMMARY_INTERVAL_MIN; // Fence seeds: lat/lon range-checked; radius capped at max (0 is valid sentinel). g_fence_lat = (g_s.applied_env_fence_lat >= -90.0f && - g_s.applied_env_fence_lat <= 90.0f) - ? g_s.applied_env_fence_lat : 0.0f; + g_s.applied_env_fence_lat <= 90.0f) + ? g_s.applied_env_fence_lat + : 0.0f; g_fence_lon = (g_s.applied_env_fence_lon >= -180.0f && - g_s.applied_env_fence_lon <= 180.0f) - ? g_s.applied_env_fence_lon : 0.0f; + g_s.applied_env_fence_lon <= 180.0f) + ? g_s.applied_env_fence_lon + : 0.0f; g_fence_radius_m = (g_s.applied_env_fence_radius_m <= GEOFENCE_RADIUS_MAX_M) - ? g_s.applied_env_fence_radius_m : 0; + ? g_s.applied_env_fence_radius_m + : 0; // ── Step 2: fetch fresh values ──────────────────────────────────────────── // ENV_OK → apply and persist the new value in applied_*. @@ -247,24 +286,30 @@ void fetchEnvOverrides(void) { EnvResult er; er = envReadDouble("vib_run_mg", val); - if (er == ENV_OK) { - float clamped = clampF(val, 1.0f, 500.0f, VIB_RUN_MG_DEFAULT); - g_vib_run_mg = clamped; + if (er == ENV_OK) + { + float clamped = clampF(val, 1.0f, 500.0f, VIB_RUN_MG_DEFAULT); + g_vib_run_mg = clamped; g_s.applied_vib_run_mg = clamped; - } else if (er == ENV_UNSET) { - g_vib_run_mg = VIB_RUN_MG_DEFAULT; - g_s.applied_vib_run_mg = 0.0f; // 0 = "never set; use compile-time default" + } + else if (er == ENV_UNSET) + { + g_vib_run_mg = VIB_RUN_MG_DEFAULT; + g_s.applied_vib_run_mg = 0.0f; // 0 = "never set; use compile-time default" } er = envReadDouble("vib_cv_max", val); - if (er == ENV_OK) { + if (er == ENV_OK) + { // CV = σ/μ; below ~0.05 is unreachably stable; above 2.0 mis-classifies // nearly all transport as running. - float clamped = clampF(val, 0.05f, 2.0f, VIB_CV_MAX_DEFAULT); - g_vib_cv_max = clamped; + float clamped = clampF(val, 0.05f, 2.0f, VIB_CV_MAX_DEFAULT); + g_vib_cv_max = clamped; g_s.applied_vib_cv_max = clamped; - } else if (er == ENV_UNSET) { - g_vib_cv_max = VIB_CV_MAX_DEFAULT; + } + else if (er == ENV_UNSET) + { + g_vib_cv_max = VIB_CV_MAX_DEFAULT; g_s.applied_vib_cv_max = 0.0f; } @@ -273,54 +318,75 @@ void fetchEnvOverrides(void) { // applyGeofenceIfChanged() validates that all three are coherent before // issuing a Notecard call. er = envReadDouble("geofence_lat", val); - if (er == ENV_OK && val >= -90.0 && val <= 90.0) { - g_fence_lat = (float)val; + if (er == ENV_OK && val >= -90.0 && val <= 90.0) + { + g_fence_lat = (float)val; g_s.applied_env_fence_lat = (float)val; - } else if (er == ENV_UNSET) { - g_fence_lat = 0.0f; + } + else if (er == ENV_UNSET) + { + g_fence_lat = 0.0f; g_s.applied_env_fence_lat = 0.0f; } er = envReadDouble("geofence_lon", val); - if (er == ENV_OK && val >= -180.0 && val <= 180.0) { - g_fence_lon = (float)val; + if (er == ENV_OK && val >= -180.0 && val <= 180.0) + { + g_fence_lon = (float)val; g_s.applied_env_fence_lon = (float)val; - } else if (er == ENV_UNSET) { - g_fence_lon = 0.0f; + } + else if (er == ENV_UNSET) + { + g_fence_lon = 0.0f; g_s.applied_env_fence_lon = 0.0f; } er = envReadDouble("geofence_radius_m", val); - if (er == ENV_OK) { + if (er == ENV_OK) + { // Validate signedness BEFORE any unsigned cast. A negative value // such as -1 wraps to 0xFFFFFFFF when cast directly to uint32_t; that // huge number then clamps to GEOFENCE_RADIUS_MAX_M (50 km), silently // activating the maximum-size fence on what was an invalid operator input. - if (val < 0.0) { - Serial.print("[ENV] geofence_radius_m="); Serial.print(val, 1); + if (val < 0.0) + { + Serial.print("[ENV] geofence_radius_m="); + Serial.print(val, 1); Serial.println(" is negative — ignored"); - } else { + } + else + { uint32_t r = (uint32_t)val; // Clamp non-zero radii to the declared operating range. Zero is the // sentinel meaning "no fence" and is never clamped. An out-of-range // value (e.g. a typo in a fleet env var) is forced to the nearest bound // and logged so the operator can diagnose it without guessing. - if (r > 0) { - if (r < GEOFENCE_RADIUS_MIN_M) { - Serial.print("[ENV] geofence_radius_m="); Serial.print(r); - Serial.print(" below minimum — clamped to "); Serial.println(GEOFENCE_RADIUS_MIN_M); + if (r > 0) + { + if (r < GEOFENCE_RADIUS_MIN_M) + { + Serial.print("[ENV] geofence_radius_m="); + Serial.print(r); + Serial.print(" below minimum — clamped to "); + Serial.println(GEOFENCE_RADIUS_MIN_M); r = GEOFENCE_RADIUS_MIN_M; - } else if (r > GEOFENCE_RADIUS_MAX_M) { - Serial.print("[ENV] geofence_radius_m="); Serial.print(r); - Serial.print(" above maximum — clamped to "); Serial.println(GEOFENCE_RADIUS_MAX_M); + } + else if (r > GEOFENCE_RADIUS_MAX_M) + { + Serial.print("[ENV] geofence_radius_m="); + Serial.print(r); + Serial.print(" above maximum — clamped to "); + Serial.println(GEOFENCE_RADIUS_MAX_M); r = GEOFENCE_RADIUS_MAX_M; } } - g_fence_radius_m = r; + g_fence_radius_m = r; g_s.applied_env_fence_radius_m = r; } - } else if (er == ENV_UNSET) { - g_fence_radius_m = 0; + } + else if (er == ENV_UNSET) + { + g_fence_radius_m = 0; g_s.applied_env_fence_radius_m = 0; } @@ -328,26 +394,34 @@ void fetchEnvOverrides(void) { // only persist the new cadence after the Notecard confirms the request so // the local summary timer and the Notecard outbound cadence stay in sync. er = envReadDouble("summary_interval_min", val); - if (er == ENV_OK) { + if (er == ENV_OK) + { uint32_t new_interval = clampU32((long)val, 60, 44640, g_summary_interval_min); - if (new_interval != g_summary_interval_min) { + if (new_interval != g_summary_interval_min) + { J *hreq = notecard.newRequest("hub.set"); JAddNumberToObject(hreq, "outbound", (int)new_interval); - if (checkedRequest(hreq)) { - g_summary_interval_min = new_interval; + if (checkedRequest(hreq)) + { + g_summary_interval_min = new_interval; g_s.applied_summary_interval_min = new_interval; - Serial.print("[ENV] summary_interval_min updated to "); Serial.println(new_interval); + Serial.print("[ENV] summary_interval_min updated to "); + Serial.println(new_interval); } } - } else if (er == ENV_UNSET) { + } + else if (er == ENV_UNSET) + { // Variable deleted in Notehub: revert to the compile-time default and // update the Notecard outbound cadence if it had been changed. - if (g_summary_interval_min != SUMMARY_INTERVAL_MIN) { + if (g_summary_interval_min != SUMMARY_INTERVAL_MIN) + { J *hreq = notecard.newRequest("hub.set"); JAddNumberToObject(hreq, "outbound", SUMMARY_INTERVAL_MIN); - if (checkedRequest(hreq)) { - g_summary_interval_min = SUMMARY_INTERVAL_MIN; - g_s.applied_summary_interval_min = 0; // 0 = "never set; use default" + if (checkedRequest(hreq)) + { + g_summary_interval_min = SUMMARY_INTERVAL_MIN; + g_s.applied_summary_interval_min = 0; // 0 = "never set; use default" Serial.println("[ENV] summary_interval_min reverted to compile-time default"); } } @@ -371,18 +445,22 @@ void fetchEnvOverrides(void) { // Cached state (fence_lat / fence_lon / fence_radius_m / fence_was_active) is // only updated after the Notecard confirms the request so that a transient // failure causes a retry on the next wake rather than silently sticking. -void applyGeofenceIfChanged(void) { - if (g_fence_radius_m == 0) { - if (g_s.fence_was_active) { +void applyGeofenceIfChanged(void) +{ + if (g_fence_radius_m == 0) + { + if (g_s.fence_was_active) + { // Re-apply periodic mode without fence parameters to clear the fence. J *req = notecard.newRequest("card.location.mode"); - JAddStringToObject(req, "mode", "periodic"); + JAddStringToObject(req, "mode", "periodic"); JAddNumberToObject(req, "seconds", GPS_PERIOD_SECONDS); - if (checkedRequest(req)) { + if (checkedRequest(req)) + { g_s.fence_was_active = false; - g_s.fence_lat = 0.0f; - g_s.fence_lon = 0.0f; - g_s.fence_radius_m = 0; + g_s.fence_lat = 0.0f; + g_s.fence_lon = 0.0f; + g_s.fence_radius_m = 0; Serial.println("[GEO] Fence cleared"); } } @@ -396,7 +474,8 @@ void applyGeofenceIfChanged(void) { (g_fence_lat >= -90.0f) && (g_fence_lat <= 90.0f); bool lon_ok = (fabsf(g_fence_lon) > 0.0001f) && (g_fence_lon >= -180.0f) && (g_fence_lon <= 180.0f); - if (!lat_ok || !lon_ok) { + if (!lat_ok || !lon_ok) + { Serial.println("[GEO] Skipped: set geofence_lat, geofence_lon, and geofence_radius_m together"); return; } @@ -404,23 +483,28 @@ void applyGeofenceIfChanged(void) { // Skip if all three cached values match — avoids redundant Notecard calls. if (fabsf(g_fence_lat - g_s.fence_lat) < 0.0001f && fabsf(g_fence_lon - g_s.fence_lon) < 0.0001f && - g_fence_radius_m == g_s.fence_radius_m) return; + g_fence_radius_m == g_s.fence_radius_m) + return; J *req = notecard.newRequest("card.location.mode"); - JAddStringToObject(req, "mode", "periodic"); + JAddStringToObject(req, "mode", "periodic"); JAddNumberToObject(req, "seconds", GPS_PERIOD_SECONDS); - JAddNumberToObject(req, "lat", g_fence_lat); - JAddNumberToObject(req, "lon", g_fence_lon); - JAddNumberToObject(req, "max", (int)g_fence_radius_m); - JAddNumberToObject(req, "minutes", 2); // confirm outside fence for 2 min - if (checkedRequest(req)) { - g_s.fence_lat = g_fence_lat; - g_s.fence_lon = g_fence_lon; - g_s.fence_radius_m = g_fence_radius_m; + JAddNumberToObject(req, "lat", g_fence_lat); + JAddNumberToObject(req, "lon", g_fence_lon); + JAddNumberToObject(req, "max", (int)g_fence_radius_m); + JAddNumberToObject(req, "minutes", 2); // confirm outside fence for 2 min + if (checkedRequest(req)) + { + g_s.fence_lat = g_fence_lat; + g_s.fence_lon = g_fence_lon; + g_s.fence_radius_m = g_fence_radius_m; g_s.fence_was_active = true; - Serial.print("[GEO] Fence: lat="); Serial.print(g_fence_lat, 5); - Serial.print(" lon="); Serial.print(g_fence_lon, 5); - Serial.print(" r="); Serial.println(g_fence_radius_m); + Serial.print("[GEO] Fence: lat="); + Serial.print(g_fence_lat, 5); + Serial.print(" lon="); + Serial.print(g_fence_lon, 5); + Serial.print(" r="); + Serial.println(g_fence_radius_m); } } @@ -431,34 +515,41 @@ void applyGeofenceIfChanged(void) { // // Engine idle (diesel, 700 RPM): periodic ~11.7 Hz vibration → low CV (0.10–0.25) // Transport (truck, road): aperiodic shock → high CV (0.50–1.0+) -EquipState classifyVibration(void) { +EquipState classifyVibration(void) +{ const float G = 9.806f; float sum = 0.0f, sum_sq = 0.0f; - for (int i = 0; i < VIB_SAMPLE_COUNT; i++) { + for (int i = 0; i < VIB_SAMPLE_COUNT; i++) + { sensors_event_t accel, gyro, temp; sox.getEvent(&accel, &gyro, &temp); float ax = accel.acceleration.x, ay = accel.acceleration.y, az = accel.acceleration.z; - float delta_mg = fabsf(sqrtf(ax*ax + ay*ay + az*az) - G) * 1000.0f / G; - sum += delta_mg; + float delta_mg = fabsf(sqrtf(ax * ax + ay * ay + az * az) - G) * 1000.0f / G; + sum += delta_mg; sum_sq += delta_mg * delta_mg; // sox.getEvent() takes ~1–2 ms; budget ~7.5 ms more → ~104 Hz net rate delayMicroseconds(7500); } - const float n = (float)VIB_SAMPLE_COUNT; - float mean = sum / n; - float var = (sum_sq / n) - (mean * mean); - float cv = (mean > 1.0f) ? sqrtf(var > 0.0f ? var : 0.0f) / mean : 1.0f; - float rms = sqrtf(sum_sq / n); - - const char *label = (rms < g_vib_run_mg) ? "IDLE" - : (cv < g_vib_cv_max) ? "RUNNING" : "TRANSPORT"; - Serial.print("[VIB] rms="); Serial.print(rms, 1); - Serial.print("mg cv="); Serial.print(cv, 3); - Serial.print(" → "); Serial.println(label); - - if (rms < g_vib_run_mg) return ST_IDLE; + const float n = (float)VIB_SAMPLE_COUNT; + float mean = sum / n; + float var = (sum_sq / n) - (mean * mean); + float cv = (mean > 1.0f) ? sqrtf(var > 0.0f ? var : 0.0f) / mean : 1.0f; + float rms = sqrtf(sum_sq / n); + + const char *label = (rms < g_vib_run_mg) ? "IDLE" + : (cv < g_vib_cv_max) ? "RUNNING" + : "TRANSPORT"; + Serial.print("[VIB] rms="); + Serial.print(rms, 1); + Serial.print("mg cv="); + Serial.print(cv, 3); + Serial.print(" → "); + Serial.println(label); + + if (rms < g_vib_run_mg) + return ST_IDLE; return (cv < g_vib_cv_max) ? ST_RUNNING : ST_TRANSPORT; } @@ -475,16 +566,27 @@ EquipState classifyVibration(void) { // • no previous sample epoch is recorded (first wake after cold boot) // • the computed delta is implausibly large (time-sync jump or stale epoch // after a reboot) — capped at 3× nominal to protect against runaway credits -void updateHourAccumulator(uint32_t now) { - uint32_t elapsed_sec = SAMPLE_INTERVAL_SEC; // nominal fallback - if (now > 0 && g_s.last_sample_epoch > 0 && now > g_s.last_sample_epoch) { +void updateHourAccumulator(uint32_t now) +{ + uint32_t elapsed_sec = SAMPLE_INTERVAL_SEC; // nominal fallback + if (now > 0 && g_s.last_sample_epoch > 0 && now > g_s.last_sample_epoch) + { uint32_t diff = now - g_s.last_sample_epoch; - if (diff <= (uint32_t)SAMPLE_INTERVAL_SEC * 3u) elapsed_sec = diff; + if (diff <= (uint32_t)SAMPLE_INTERVAL_SEC * 3u) + elapsed_sec = diff; } const float delta_h = (float)elapsed_sec / 3600.0f; - if (g_s.prev_state == ST_RUNNING) { g_s.run_h_today += delta_h; g_s.run_h_total += delta_h; } - else if (g_s.prev_state == ST_TRANSPORT) { g_s.transport_h_today += delta_h; } - if (now > 0) g_s.last_sample_epoch = now; + if (g_s.prev_state == ST_RUNNING) + { + g_s.run_h_today += delta_h; + g_s.run_h_total += delta_h; + } + else if (g_s.prev_state == ST_TRANSPORT) + { + g_s.transport_h_today += delta_h; + } + if (now > 0) + g_s.last_sample_epoch = now; } // ── Pending-event ring-buffer helpers ───────────────────────────────────────── @@ -495,16 +597,19 @@ void updateHourAccumulator(uint32_t now) { // dropped rather than overwriting the oldest billable evidence. // Returns true if the event was successfully enqueued. bool enqueueEvent(const char *tag, uint32_t epoch, - float session_min, float run_h_total) { - if (g_s.evq_count >= PENDING_QUEUE_DEPTH) { - Serial.print("[EVENT] queue full — dropping "); Serial.println(tag); + float session_min, float run_h_total) +{ + if (g_s.evq_count >= PENDING_QUEUE_DEPTH) + { + Serial.print("[EVENT] queue full — dropping "); + Serial.println(tag); return false; } uint8_t tail = (g_s.evq_head + g_s.evq_count) % PENDING_QUEUE_DEPTH; PendingEvent &e = g_s.evq[tail]; strncpy(e.tag, tag, sizeof(e.tag) - 1); e.tag[sizeof(e.tag) - 1] = '\0'; - e.epoch = epoch; + e.epoch = epoch; e.session_min = session_min; e.run_h_total = run_h_total; g_s.evq_count++; @@ -516,8 +621,10 @@ bool enqueueEvent(const char *tag, uint32_t epoch, // Returns true if the queue was empty (nothing to send) or the oldest event // was successfully acknowledged. Returns false if the send failed; the head // is not advanced and the event will be retried on the next wake. -bool sendNextPendingEvent(void) { - if (g_s.evq_count == 0) return true; +bool sendNextPendingEvent(void) +{ + if (g_s.evq_count == 0) + return true; PendingEvent &e = g_s.evq[g_s.evq_head]; @@ -535,26 +642,36 @@ bool sendNextPendingEvent(void) { // retransmitted duplicate will carry the same epoch and event tag as the // original. A separate hub.sync call below requests prompt delivery // without tying it to the note.add retry logic. - for (int attempt = 0; attempt < 2; attempt++) { + for (int attempt = 0; attempt < 2; attempt++) + { J *req = notecard.newRequest("note.add"); JAddStringToObject(req, "file", "equip_event.qo"); J *body = JAddObjectToObject(req, "body"); - JAddStringToObject(body, "event", e.tag); + JAddStringToObject(body, "event", e.tag); JAddNumberToObject(body, "session_min", e.session_min); JAddNumberToObject(body, "run_h_total", e.run_h_total); - JAddNumberToObject(body, "epoch", (JNUMBER)e.epoch); // transition timestamp + JAddNumberToObject(body, "epoch", (JNUMBER)e.epoch); // transition timestamp J *rsp = notecard.requestAndResponse(req); - if (!rsp) { - Serial.print("[EVENT] no response (attempt "); Serial.print(attempt + 1); Serial.println(")"); + if (!rsp) + { + Serial.print("[EVENT] no response (attempt "); + Serial.print(attempt + 1); + Serial.println(")"); continue; } const char *err = JGetString(rsp, "err"); bool ok = (!err || *err == '\0'); - if (!ok) { Serial.print("[EVENT] err: "); Serial.println(err); } + if (!ok) + { + Serial.print("[EVENT] err: "); + Serial.println(err); + } notecard.deleteResponse(rsp); - if (ok) { - Serial.print("[EVENT] "); Serial.println(e.tag); - g_s.evq_head = (g_s.evq_head + 1) % PENDING_QUEUE_DEPTH; + if (ok) + { + Serial.print("[EVENT] "); + Serial.println(e.tag); + g_s.evq_head = (g_s.evq_head + 1) % PENDING_QUEUE_DEPTH; g_s.evq_count--; // Request prompt delivery as a separate fire-and-forget call so // time-critical billing events are not held until the next @@ -562,11 +679,13 @@ bool sendNextPendingEvent(void) { // Notecard's responsibility; this call failing is non-fatal. J *syncReq = notecard.newRequest("hub.sync"); J *syncRsp = notecard.requestAndResponse(syncReq); - if (syncRsp) notecard.deleteResponse(syncRsp); + if (syncRsp) + notecard.deleteResponse(syncRsp); return true; } } - Serial.print("[EVENT] failed to queue "); Serial.println(e.tag); + Serial.print("[EVENT] failed to queue "); + Serial.println(e.tag); return false; } @@ -574,30 +693,43 @@ bool sendNextPendingEvent(void) { // Returns true if the note was successfully queued. The caller updates // last_summary_epoch and resets the daily accumulators only on success, so a // failed send retries on the next wake rather than silently dropping the record. -bool sendSummary(void) { +bool sendSummary(void) +{ float bat_v = getBatteryVoltage(); - for (int attempt = 0; attempt < 2; attempt++) { + for (int attempt = 0; attempt < 2; attempt++) + { J *req = notecard.newRequest("note.add"); JAddStringToObject(req, "file", "equip_summary.qo"); J *body = JAddObjectToObject(req, "body"); - JAddNumberToObject(body, "run_h", g_s.run_h_today); + JAddNumberToObject(body, "run_h", g_s.run_h_today); JAddNumberToObject(body, "run_h_total", g_s.run_h_total); JAddNumberToObject(body, "transport_h", g_s.transport_h_today); - JAddNumberToObject(body, "bat_v", bat_v); - JAddNumberToObject(body, "fault_ct", g_s.evq_overflow_count); + JAddNumberToObject(body, "bat_v", bat_v); + JAddNumberToObject(body, "fault_ct", g_s.evq_overflow_count); J *rsp = notecard.requestAndResponse(req); - if (!rsp) { - Serial.print("[SUMMARY] no response (attempt "); Serial.print(attempt + 1); Serial.println(")"); + if (!rsp) + { + Serial.print("[SUMMARY] no response (attempt "); + Serial.print(attempt + 1); + Serial.println(")"); continue; } const char *err = JGetString(rsp, "err"); bool ok = (!err || *err == '\0'); - if (!ok) { Serial.print("[SUMMARY] err: "); Serial.println(err); } + if (!ok) + { + Serial.print("[SUMMARY] err: "); + Serial.println(err); + } notecard.deleteResponse(rsp); - if (ok) { - Serial.print("[SUMMARY] run_h="); Serial.print(g_s.run_h_today, 2); - Serial.print(" total="); Serial.print(g_s.run_h_total, 1); - Serial.print(" bat_v="); Serial.println(bat_v, 2); + if (ok) + { + Serial.print("[SUMMARY] run_h="); + Serial.print(g_s.run_h_today, 2); + Serial.print(" total="); + Serial.print(g_s.run_h_total, 1); + Serial.print(" bat_v="); + Serial.println(bat_v, 2); return true; } } @@ -606,23 +738,29 @@ bool sendSummary(void) { } // ── Utilities ───────────────────────────────────────────────────────────────── -uint32_t getEpoch(void) { +uint32_t getEpoch(void) +{ J *rsp = notecard.requestAndResponse(notecard.newRequest("card.time")); uint32_t t = 0; - if (rsp) { + if (rsp) + { const char *err = JGetString(rsp, "err"); - if (!err || *err == '\0') t = (uint32_t)JGetNumber(rsp, "time"); + if (!err || *err == '\0') + t = (uint32_t)JGetNumber(rsp, "time"); notecard.deleteResponse(rsp); } return t; } -float getBatteryVoltage(void) { +float getBatteryVoltage(void) +{ J *rsp = notecard.requestAndResponse(notecard.newRequest("card.voltage")); float v = 0.0f; - if (rsp) { + if (rsp) + { const char *err = JGetString(rsp, "err"); - if (!err || *err == '\0') v = (float)JGetNumber(rsp, "value"); + if (!err || *err == '\0') + v = (float)JGetNumber(rsp, "value"); notecard.deleteResponse(rsp); } return v; @@ -630,15 +768,16 @@ float getBatteryVoltage(void) { // ── Sleep — persist state and cut host power via ATTN ───────────────────────── // NotePayloadSaveAndSleep serialises g_s into Notecard flash, then issues -// card.attn mode:sleep. On Notecarrier CX v1.3, ATTN and EN are separate +// card.attn mode:sleep. On Notecarrier CX, ATTN and EN are separate // header pins; an external jumper wire from ATTN to EN must be installed for // the Notecard to gate the Cygnet's 3.3V host rail (see README §4). With the // jumper in place, SAMPLE_INTERVAL_SEC later the Cygnet powers up and re-enters // setup(). Without the jumper the host stays powered and loop() takes over as // the (higher-power) fallback path. -void goToSleep(void) { +void goToSleep(void) +{ NotePayloadDesc payload = {0, 0, 0}; NotePayloadAddSegment(&payload, SEG_ID, &g_s, sizeof(g_s)); NotePayloadSaveAndSleep(&payload, SAMPLE_INTERVAL_SEC, NULL); - delay(15000); // should not return; loop() retries setup() as fallback + delay(15000); // should not return; loop() retries setup() as fallback } diff --git a/78-rail-car-condition-interchange-tracker/README.md b/78-rail-car-condition-interchange-tracker/README.md index 6772b405..d99b3a2d 100644 --- a/78-rail-car-condition-interchange-tracker/README.md +++ b/78-rail-car-condition-interchange-tracker/README.md @@ -69,7 +69,7 @@ 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 onboard Cygnet STM32L433 host — no separate MCU needed. I²C, six analog inputs, nine digital I/O, and M.2 Notecard slot. See the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/). | +| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with onboard Cygnet STM32L433 host — no separate MCU needed. I²C, six analog inputs, nine digital I/O, and M.2 Notecard slot. See the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/). | | [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Single M.2 module with LTE-M/NB-IoT/GPRS + Skylo NTN satellite. 500 MB cellular + 10 KB satellite bundled; no monthly fees. **Ships with its Skylo-certified LTE/NTN antenna — do not substitute another antenna** without a CTIA/OTA delta test report; Skylo may block uncertified devices. See [Notecard for Skylo datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/). | | [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter for bench power validation. See [§9](#9-validation-and-testing). Not deployed to the field. See the [Mojo datasheet](https://dev.blues.io/datasheets/mojo-datasheet/). | | [Blues Scoop](https://shop.blues.com/products/scoop?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Lithium-ion capacitor (250 F) peak-current buffer, wired **inline** between the 5 V boost module and the Notecarrier CX `+VBAT` rail. The Scoop has two connectors and two sets of header pins: **J1 (CHG)** JST input (4.8–24 V; headers **J3** are a through-hole alternative to J1) and **J2 (OUT)** JST output (2.5–3.8 V; headers **J4** are a through-hole alternative to J2). The 5 V boost module output connects to J1/J3; Scoop J2/J4 connects to Notecarrier CX `+VBAT`. The internal LiC charges from J1 between radio sessions and supplements the supply rail through J2 during high-current cellular and satellite bursts. **J1 requires ≥ 4.8 V — do not connect it directly to the LiPo (max 4.2 V) or to a raw solar panel.** It is **not** a LiPo charger and does not replace the solar charge controller. See the [Scoop datasheet](https://dev.blues.io/datasheets/scoop-datasheet/). | @@ -91,7 +91,7 @@ Notecard for Skylo ships with bundled cellular and satellite connectivity — 50 ## 5. Wiring and Assembly -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. Notecard for Skylo seats into the carrier's M.2 slot; its MAIN u.FL port connects to the included Skylo-certified LTE/NTN antenna, and its GPS u.FL port connects to the separate GNSS antenna via the u.FL-to-SMA pigtail adapter listed in the BOM. The Mojo sits inline between Scoop J2 and the Notecarrier `+VBAT` during bench testing (remove for field deployment). +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. Notecard for Skylo seats into the carrier's M.2 slot; its MAIN u.FL port connects to the included Skylo-certified LTE/NTN antenna, and its GPS u.FL port connects to the separate GNSS antenna via the u.FL-to-SMA pigtail adapter listed in the BOM. The Mojo sits inline between Scoop J2 and the Notecarrier `+VBAT` during bench testing (remove for field deployment). ![Wiring diagram: ADXL345 and reed switch on I²C/D5, MPRLS and DS18B20 (TANK_CAR), antennas on u.FL ports, and the inline Scoop power path: solar → charge controller → LiPo → 5 V boost → Scoop J1 (CHG, ≥4.8 V) → Scoop J2 (OUT, 2.5–3.8 V) → Notecarrier CX +VBAT](diagrams/02-wiring-assembly.svg) diff --git a/79-utility-distribution-transformer-load-monitor/README.md b/79-utility-distribution-transformer-load-monitor/README.md index a8e2549f..3f2c1cb5 100644 --- a/79-utility-distribution-transformer-load-monitor/README.md +++ b/79-utility-distribution-transformer-load-monitor/README.md @@ -124,7 +124,7 @@ All Blues hardware ships with an active SIM including 500 MB of data and 10 year ![Wiring diagram: CT bias circuits (22 Ω burden + 10 kΩ divider + 10 µF cap) on A0/A1/A2; MCP9808 on I²C; 120 VAC → slow-blow fuse → IRM-10-5 → Mojo (bench) → +VBAT; outdoor SMA antenna via u.FL pigtail](diagrams/02-wiring-assembly.svg) -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. The Notecard Cell+WiFi (MBGLW) seats into the carrier's M.2 slot. The Mojo, if used for bench validation, sits inline between the 5V supply and the Notecarrier's +VBAT pad; connect its Qwiic cable to any available Qwiic port on the Notecarrier so it can log charge to the Notecard. The transformer firmware does not poll the Mojo — its coulomb-count readings appear as separate Notecard events in Notehub and are a bench commissioning tool only; the Mojo is not installed in the deployed unit. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard Cell+WiFi (MBGLW) seats into the carrier's M.2 slot. The Mojo, if used for bench validation, sits inline between the 5V supply and the Notecarrier's +VBAT pad; connect its Qwiic cable to any available Qwiic port on the Notecarrier so it can log charge to the Notecard. The transformer firmware does not poll the Mojo — its coulomb-count readings appear as separate Notecard events in Notehub and are a bench commissioning tool only; the Mojo is not installed in the deployed unit. ### Bias circuit (build once per CT channel, identical for A, B, and C) diff --git a/81-commercial-tenant-sub-metering-bridge/README.md b/81-commercial-tenant-sub-metering-bridge/README.md index 82736705..a0687adc 100644 --- a/81-commercial-tenant-sub-metering-bridge/README.md +++ b/81-commercial-tenant-sub-metering-bridge/README.md @@ -99,7 +99,7 @@ All Blues hardware ships with an active SIM including 500 MB of data and 10 year ![Wiring: 4 Rogowski coils on A0–A3, ZMPT101B voltage on A4, MCP6004 op-amp integrator; cellular antenna via u.FL; 120 VAC → 5 V supply → Mojo (bench) → +VBAT](diagrams/02-wiring-assembly.svg) -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. The Notecard MBGLW seats into the M.2 slot on the carrier. Mojo (bench-only) sits inline between the 5 V supply output and the Notecarrier CX `+VBAT` pad. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. The Notecard MBGLW seats into the M.2 slot on the carrier. Mojo (bench-only) sits inline between the 5 V supply output and the Notecarrier CX `+VBAT` pad. 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 40aec9f0..81bfb468 100644 --- a/82-aerial-lift-rental-equipment-battery-health-monitor/README.md +++ b/82-aerial-lift-rental-equipment-battery-health-monitor/README.md @@ -96,7 +96,7 @@ All Blues hardware ships with an active SIM and no activation fees or monthly co ![Wiring: INA228 on I²C, NTC thermistor on ADC, optional ACS758 on A1 or CAN BMS on SPI; MAIN antenna for cellular + Skylo NTN; 12 V pack aux → buck regulator → Mojo (bench) → +VBAT](diagrams/02-wiring-assembly.svg) -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. Notecard for Skylo seats in the carrier's M.2 slot. The Mojo sits inline between the 5V supply output and the Notecarrier's VBAT+ pad during bench validation. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. Notecard for Skylo seats in the carrier's M.2 slot. The Mojo sits inline between the 5V supply output and the Notecarrier's VBAT+ pad during bench validation. @@ -188,7 +188,7 @@ In bench builds where pack current stays below 10 A, the INA228 can simultaneous `readCurrent()` is valid in this configuration because the 15 mΩ shunt bridges `VIN+` and `VIN–`. `setShunt()` is called with the onboard shunt values (`0.015 Ω`, `10 A`) when `BENCH_ONLY 1`; no register change is needed for bench testing. -> **Note on the Notecarrier CX v1.3 errata.** The v1.3 board silkscreen has the `MOSI` and `MISO` labels swapped on the dual 16-pin header. When wiring the CAN module's SPI, verify pin function in the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) rather than trusting the header labels. +> **Note on the Notecarrier CX errata.** The v1.3 board silkscreen has the `MOSI` and `MISO` labels swapped on the dual 16-pin header. When wiring the CAN module's SPI, verify pin function in the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) rather than trusting the header labels. **Thermistor (A0):** - Notecarrier CX **+3V3** → 10 kΩ 1% series resistor → Notecarrier CX **A0** diff --git a/83-off-grid-solar-battery-site-controller/README.md b/83-off-grid-solar-battery-site-controller/README.md index 1da39b6e..1e24907a 100644 --- a/83-off-grid-solar-battery-site-controller/README.md +++ b/83-off-grid-solar-battery-site-controller/README.md @@ -111,7 +111,7 @@ All Blues hardware ships with an active SIM including 500 MB of data and 10 year ![Wiring: SmartShunt on UART1, SmartSolar MPPT on UART2 (both VE.Direct); MAIN antenna for cellular or satellite; site 5 V bus → DC-DC step-down → Mojo (bench) → +VBAT](diagrams/02-wiring-assembly.svg) -All host I/O uses the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) 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. diff --git a/84-remote-cabinet-backup-battery-sentinel/README.md b/84-remote-cabinet-backup-battery-sentinel/README.md index fb9a3a35..136d02d8 100644 --- a/84-remote-cabinet-backup-battery-sentinel/README.md +++ b/84-remote-cabinet-backup-battery-sentinel/README.md @@ -130,7 +130,7 @@ This BOM targets a **positive-referenced DC bus** — the INA228 high-side sensi -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header and Qwiic connector. The Notecard Cell+WiFi seats into the M.2 slot. +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header and Qwiic connector. The Notecard Cell+WiFi seats into the M.2 slot. **Cabinet bus to Notecarrier CX power chain:** @@ -142,7 +142,7 @@ DC-DC module output (5 V regulated) → Notecarrier CX +VBAT header pin Cabinet battery bus (–) / cabinet ground → Notecarrier CX GND header pin ``` -Mount the DC-DC module inside the enclosure and verify its input-voltage range covers your cabinet supply before wiring. The Notecarrier CX's USB-C connector is an equivalent 5 V power entry point if a USB-C cable is more convenient than the header pin for your enclosure layout. See the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) for +VBAT and GND pin locations on the dual 16-pin header. +Mount the DC-DC module inside the enclosure and verify its input-voltage range covers your cabinet supply before wiring. The Notecarrier CX's USB-C connector is an equivalent 5 V power entry point if a USB-C cable is more convenient than the header pin for your enclosure layout. See the [Notecarrier CX datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) for +VBAT and GND pin locations on the dual 16-pin header. The 2000 mAh LiPo on the JST connector is charged from the regulated 5 V supply whenever the cabinet bus is present. During a normal mains outage the sentinel continues drawing power from the cabinet battery via the DC-DC converter — the LiPo does not take over yet. This is intentional: the sentinel is riding on the battery it monitors, directly observing the discharge as it happens. The LiPo becomes the primary power source only after the monitored battery is deeply depleted, the bus voltage drops below the DC-DC converter's minimum input threshold, or the battery is disconnected — extending the reporting tail beyond the battery's own capacity. If the design requirement is full electrical independence from the monitored battery from the moment mains fails, an isolated power supply fed from a separate source is needed instead. diff --git a/85-cellular-medication-adherence-pillbox/README.md b/85-cellular-medication-adherence-pillbox/README.md index fac3c4e1..ce850bec 100644 --- a/85-cellular-medication-adherence-pillbox/README.md +++ b/85-cellular-medication-adherence-pillbox/README.md @@ -81,7 +81,7 @@ All Blues hardware ships with an active SIM including 500 MB of data and 10 year ![Wiring and assembly](diagrams/02-wiring-assembly.svg) -The Notecard Cell+WiFi (NOTE-MBGLW) seats into the Notecarrier CX's M.2 connector and is powered from the same VBAT rail. All host I/O lands on the [Notecarrier CX dual 16-pin header](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/). +The Notecard Cell+WiFi (NOTE-MBGLW) seats into the Notecarrier CX's M.2 connector and is powered from the same VBAT rail. All host I/O lands on the [Notecarrier CX dual 16-pin header](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/). **Micro-switch wiring — all 7 compartments are wired identically:** 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 8f4885bd..ee72d974 100644 --- a/88-reefer-trailer-cold-chain-door-event-monitor/README.md +++ b/88-reefer-trailer-cold-chain-door-event-monitor/README.md @@ -39,7 +39,7 @@ Both failure modes have a common root: nobody can see inside the trailer while i ![System Architecture](diagrams/01-system-architecture.svg) -**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-3/) 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. @@ -76,7 +76,7 @@ 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 STM32 host — no separate MCU board needed. Supports `card.attn` power gating for deep-sleep between samples. See the [datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/). | +| [Notecarrier CX](https://shop.blues.com/products/notecarrier-cx?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Integrated carrier with an embedded Cygnet STM32 host — no separate MCU board needed. Supports `card.attn` power gating for deep-sleep between samples. See the [datasheet](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/). | | [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/products/notecard-for-skylo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Cellular (LTE-M / NB-IoT / GPRS) + Skylo NTN satellite module with built-in GNSS and onboard WiFi antenna (Quectel BG95-S5 cellular/satellite modem + Silicon Labs WFM200S WiFi). Automatic network selection requires no firmware changes. Bundles 500 MB cellular data + 10 KB satellite data over 10 years with no activation fees. See the [datasheet](https://dev.blues.io/datasheets/notecard-datasheet/note-nbglwx/). | | [Blues Mojo](https://shop.blues.com/products/mojo?utm_source=dev-blues&utm_medium=web&utm_campaign=store-link) | 1 | Coulomb counter on the power rail. Required during commissioning and validation for ground-truth energy measurement (see [§9](#9-validation-and-testing)). Remove Mojo and the Qwiic cable before production deployment — the inline current draw and quiescent load are not needed in the field. See the [datasheet](https://dev.blues.io/datasheets/mojo-datasheet/). | | [Adafruit DS18B20 Waterproof Temperature Sensor](https://www.adafruit.com/product/381) (#381) | 2 | 1-Wire stainless-steel probe; −55 °C to +125 °C range; ±0.5 °C from −10 °C to +85 °C; includes a 4.7 kΩ pull-up resistor. One probe near the front evaporator; one near the rear return-air panel. | @@ -97,7 +97,7 @@ All Blues hardware ships with an active SIM; no separate SIM purchase or activat ![Wiring and Assembly](diagrams/02-wiring-assembly.svg) -All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) dual 16-pin header. Notecard for Skylo seats into the M.2 slot; cellular, satellite, and GNSS antennas connect via u.FL leads to externally-mounted antennas. If bench-validating with the Mojo, it sits inline between the 5 V supply and the Notecarrier's `+VBAT` pad, reporting cumulative mAh to the Notecard over Qwiic (I²C). +All host I/O lands on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) dual 16-pin header. Notecard for Skylo seats into the M.2 slot; cellular, satellite, and GNSS antennas connect via u.FL leads to externally-mounted antennas. If bench-validating with the Mojo, it sits inline between the 5 V supply and the Notecarrier's `+VBAT` pad, reporting cumulative mAh to the Notecard over Qwiic (I²C). **Power (production, no Mojo):** - Pololu D24V22F5 `VIN` → inline fuse holder (3 A ATO blade fuse fitted; see BOM) → trailer 12 V DC positive. Mount the fuse holder on the positive lead as close to the 12 V source as practical. See the field installation caution below. diff --git a/89-pallet-attached-cold-chain-logger/README.md b/89-pallet-attached-cold-chain-logger/README.md index 7111c7f7..5f5cf8ec 100644 --- a/89-pallet-attached-cold-chain-logger/README.md +++ b/89-pallet-attached-cold-chain-logger/README.md @@ -36,7 +36,7 @@ This project uses [Notecard for Skylo (NOTE-NBGLWX)](https://shop.blues.com/prod ![System architecture](diagrams/01-system-architecture.svg) -**Device-side responsibilities.** Bolted to the pallet exterior, the Cygnet STM32L4 host on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-3/) wakes once every five minutes and does the entire day's work in a few seconds. It reads the PT100/MAX31865 for cargo-air temperature, the SHT41 for humidity, and the VEML7700 for interior light; pulls accumulated motion events and current orientation from the Notecard's built-in accelerometer via `card.motion`; updates the shipment-state model; evaluates five threshold rule categories (temperature is two-sided — `temp_low` or `temp_high` — for six alert types in all); appends a per-sample log entry; and emits any alert or state-change Notes the rules produce before returning to sleep. +**Device-side responsibilities.** Bolted to the pallet exterior, the Cygnet STM32L4 host on the [Notecarrier CX](https://dev.blues.io/datasheets/notecarrier-datasheet/notecarrier-cx-v1-7/) wakes once every five minutes and does the entire day's work in a few seconds. It reads the PT100/MAX31865 for cargo-air temperature, the SHT41 for humidity, and the VEML7700 for interior light; pulls accumulated motion events and current orientation from the Notecard's built-in accelerometer via `card.motion`; updates the shipment-state model; evaluates five threshold rule categories (temperature is two-sided — `temp_low` or `temp_high` — for six alert types in all); appends a per-sample log entry; and emits any alert or state-change Notes the rules produce before returning to sleep. **Shipment-state model.** After each motion and light read, the firmware evaluates the current shipment state: - **DWELL** — confirmed by `dwell_confirm_samples` consecutive low-motion samples (motion < `transit_motion_min` per interval). During dwell, both the `cargo_data.qo` summary interval and the Notecard hub.set outbound cadence are multiplied by `dwell_batch_factor` (default 4×), reducing both the number of summary notes queued per session and the number of outbound sessions per hour. `applyDynamicOutbound()` re-issues `hub.set` whenever the state transitions in or out of DWELL.