Use Your AI Agent to Talk to Your Products with Notehub IQ

Blues Developers
What’s New
Resources
API Reference
Notecard and Notehub API documentation
Blog
Technical articles for developers
Connected Product Guidebook
In-depth guides for connected product development
Developer Certification
Get certified on wireless connectivity with Blues
Newsletter
The monthly Blues developer newsletter
Terminal
Connect to a Notecard in your browser
Webinars
Listing of Blues technical webinars
Blues.comNotehub
Shop
Docs
Help
Support DocsNotehub StatusSecurity AdvisoriesVisit our Forum
Sign In
Docs Home
What’s New
Resources
API Reference
Notecard and Notehub API documentation
Blog
Technical articles for developers
Connected Product Guidebook
In-depth guides for connected product development
Developer Certification
Get certified on wireless connectivity with Blues
Newsletter
The monthly Blues developer newsletter
Terminal
Connect to a Notecard in your browser
Webinars
Listing of Blues technical webinars
Blues.comNotehub
Shop
Docs
Example Apps
Samples
Building a Simple Asset Tracker
Building Edge AI Applications
Continuous Asset Tracking with External GPS and Immediate Location Sync
Controlling Device State and Receiving Acknowledgment
Managing a Notecard's WiFi Network Remotely
Putting a Host to Sleep Between Sensor Readings
Routing Data from Notehub to a Custom Cloud Endpoint
Using LEDs and NeoPixels to Monitor Notecard Status
Accelerators
Asset Location Tracking
Heavy Equipment Hours-of-Use & Utilization Tracker
Returnable Container / Tote Pool Tracker
Untethered Trailer and Chassis Fleet Tracker
Asset Performance Optimization
Legacy Diesel Generator Fleet Performance Uplift
Solar Array String-Level Performance Dashboard
Battery Management Systems
Aerial Lift / Rental Equipment Battery Health Monitor
Off-Grid Solar Battery Site Controller
Remote Cabinet Backup Battery Sentinel
Downtime Prevention
Municipal Wastewater Lift Station Monitor
Rooftop HVAC Predictive Maintenance
VFD Pump Predictive Maintenance
Energy Monitoring Solutions
Commercial Tenant Sub-Metering Bridge
EV Charger Session & Utilization Monitor
Utility Distribution Transformer Load Monitor
Energy Savings
Commercial Plug-Load & After-Hours Waste Dashboard
Demand-Response Solar + Battery Dispatcher
Multi-Site Walk-In Cooler Energy & Setpoint Monitor
Industrial Equipment Monitoring
CNC Machine Spindle Load and Cycle Time Tracker
Injection Molding Shot-to-Shot Process Monitor
Trailer Manufacturer Connected Trailer Platform
Loss Prevention
Construction Equipment Anti-Theft Tracker with Immobilizer
Reefer Trailer Cold-Chain & Door-Event Monitor
Remote Patient Monitoring
Cellular Medication Adherence Pillbox
Post-Discharge Vitals Relay Hub
Remotely Monitored Assets
Off-Grid Livestock Water Tank Monitor
Remote Apiary Hive Health Monitor
Safety Assurance
Construction Site Environmental & Noise Exposure Monitor
Lone Worker Panic and Fall Detection Beacon
Regulatory-Grade Pharmacy and Lab Cold Storage Audit Monitor
Supply Chain Tracking
Pallet-Attached Cold Chain Logger
Rail Car Condition & Interchange Tracker
Truck Roll Reduction
Commercial Grease Interceptor Level Monitor
Propane / LPG Tank Fill Telemetry
homechevron_rightExample Appschevron_rightAcceleratorschevron_rightConstruction Site Environmental & Noise Exposure Monitor

Construction Site Environmental & Noise Exposure Monitor

Construction Site Environmental & Noise Exposure Monitor banner
note

This reference application is here to give you ideas and help you get started quickly. It's built on specific hardware that may not match yours, so focus on the sections that fit your use case. If you'd like to talk through your project and whether it's a good fit for Blues, reach out.

Building with an AI assistant? Point it at this accelerator's firmware in firmware/construction_env_monitor/ so it can learn how the device works and help you adapt it to your hardware.

This project is a safety assurance reference design that puts a GPS-stamped area monitor on a construction site. It tracks PM2.5, PM10, and dB(A) sound levels continuously and in real time, with no site WiFi, power outlets, or IT coordination required.

1. Project Overview

The problem. OSHA requires construction sites to document worker exposure to silica dust, general particulate matter, and noise. Under OSHA 29 CFR 1926.1153, air monitoring is mandatory when workers may be exposed to respirable crystalline silica above the action level (25 µg/m³). OSHA's noise standard (29 CFR 1926.52) likewise requires monitoring and protection when the TWA (time-weighted average) noise level reaches 85 dB(A) over an 8-hour shift. Compliant silica exposure documentation means filter-based personal samplers analyzed by X-ray diffraction (NIOSH Method 7500). Compliant noise documentation means a dosimeter worn by each worker for a full shift. Both methods share the same weakness: the data shows up too late to act on. By the time anyone reviews it, the shift is over and the chance to intervene in real time is gone.

This project is a solar-powered cellular area monitor, a fixed enclosure mounted on the site perimeter that tracks PM2.5, PM10, and dB(A) levels continuously and sends geo-stamped summaries over cellular. It complements, rather than replaces, personal silica sampling and noise dosimetry. When the area monitor flags high dust or high noise, a safety officer can redirect workers right away, deploy respirators or hearing protection, and send personal monitoring to the affected crew. A PM2.5/PM10 optical particle sensor and an analog sound level meter take a sample every 5 minutes near the active exposure zone. The device collects those readings and sends them as a geo-stamped summary every 30 minutes. If a reading crosses a threshold (say, a concrete saw starts up near the sensor), the device sends an alert Note as soon as it detects the breach in a sample. In the worst case, detection takes one full sample interval (5 minutes) plus measurement time (~55 seconds). Then the alert leaves the device over cellular, instead of waiting hours for an end-of-shift download.

Why Notecard. A construction site has no WiFi of its own. There's no permanent network to connect to and no IT department to hand out credentials. Meanwhile the general contractor (GC) is responsible for compliance across a dozen subcontractors, each bringing their own tools and their own hazards to the same site. The GC can't count on a sub's WiFi hotspot, a temporary access point that might get moved, or a site trailer that's offline half the time. Cellular is the only connectivity a GC can deploy and own with its own equipment. Prepaid global cellular on the Notecard Cell+WiFi means nobody has to buy SIMs site by site, and the same hardware, firmware, and enclosure deploy the same way on every site in a GC's portfolio without a single network form to fill out. When the site moves in three months, the enclosure moves with the crew.

New to Blues?

Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.

Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.

Deployment scenario. A weatherproof NEMA 4X enclosure goes on a temporary fence post, T-bar stake, or equipment cage near the active work area. For dust monitoring, put the sensor downwind of the dust-generating task so the plume passes through the sensor instead of away from it. Upwind, it will miss the plume entirely. For noise monitoring, keep the enclosure within 5–10 meters of the primary noise source. For particulate background subtraction, put a second unit upwind of the site as a baseline. The arithmetic difference between the two PM concentration readings (µg/m³) approximates how much particulate the task itself is adding. A second sound monitor upwind or in a quieter reference area gives you useful context, but you can't meaningfully background-subtract dB(A) values with simple arithmetic. Decibels are a logarithmic measure of acoustic energy, so isolating the task's contribution means working in the energy domain: convert to intensity in W/m², subtract, then convert back. Subtracting the dB numbers directly is acoustically incorrect. A 6V solar panel mounts flat on the enclosure lid, and the LiPo battery inside carries the device overnight between solar charging sessions. You don't need shore power or a network drop, and nobody has to download data every day.

2. System Architecture

System architecture: PMSA003I + SEN0232 → Notecarrier CX (STM32 host + Notecard MBGLW) → cellular → Notehub → safety alerts and compliance archive

Device-side responsibilities. The order of operations on the Notecarrier CX's STM32 host keeps the PM sensor's warm-up out of the sound measurement. Every 5 minutes the host wakes and samples sound for 15 seconds first, before begin_I2C() initializes the PMSA003I. The PMSA003I stays powered between wakes, though, so its fan noise is still in the dB(A) window (see Limitations). Then the host initializes the PM sensor, waits 30 seconds for the laser and fan to stabilize, and averages 10 readings over the next 10 seconds. Each sample goes into a rolling window that just stays in the host's RAM through the sleep. Between cycles the host sleeps in STM32 STOP2 (a few µA) until the Notecard's ATTN pin wakes it via card.attn. Queued Notes travel from the host to the Notecard over I²C through the note-arduino library's JAdd* helpers, so the host never hand-builds JSON or sends modem AT commands.

Notecard responsibilities. The Notecard buffers each Note in its on-device queue and opens a cellular session on the configured hub.set outbound cadence (30 minutes by default). It treats any sync:true alert Note as an immediate interrupt. When a sample catches a saw running near the sensor, the alert leaves the device on a session opened within the next few seconds. GNSS is the Notecard's job too. Its built-in receiver gets a site fix on first boot, re-acquires every 4 hours by default, and embeds the last-known fix in every outbound Note. Because the receiver keeps a cached fix across power cycles, the first card.location response after the enclosure moves to a new site can still return the previous site's coordinates. The firmware guards against that by treating the first non-zero response after boot as a baseline. It only flips location_valid to true when a later response carries a newer time timestamp. See Limitations. Environment variables come back down from the Blues Notehub cloud service on each inbound sync, so a GC can retune the dB(A) or PM thresholds on the fly without rolling a truck to the site.

Notehub responsibilities. Each enclosure's embedded global SIM puts it on carrier cellular networks worldwide, and its data reaches Notehub over the Internet. Notehub ingests and stores every event and applies project-level routes. Exposure summaries (env_summary.qo) and threshold alerts (env_alert.qo) land in separate Notefiles, so one device can feed both the safety officer's inbox and the long-term compliance archive with no per-Note filter logic in between. Notehub also adds where_lat, where_lon, and related location metadata to every event, based on the Notecard's last-known GNSS fix.

Routing to the cloud (high level). Notehub can route to HTTP, MQTT, AWS IoT Core, Azure IoT Hub, GCP Pub/Sub, Snowflake, and several other destinations. Route setup depends on your project, so see the Notehub routing documentation for details. This project doesn't ship with any specific downstream endpoint.

3. Technical Summary

  1. Clone and configure: Download this repo and open firmware/construction_env_monitor/construction_env_monitor_helpers.h. Replace the PRODUCT_UID constant with your Notehub project UID (see §6 step 1).
  2. Build and flash: Install the libraries (arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC" "Adafruit PM25 AQI Sensor"), then build with the board options below. They match firmware/construction_env_monitor/sketch.yaml, so you can also leave out --fqbn when you run from the sketch directory. The usb=none board option turns off the USB CDC stack, which has to be off for the host to sleep (see §7):
    arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/construction_env_monitor/
    arduino-cli upload -p /dev/ttyACM0 --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/construction_env_monitor/
  3. Power and observe: Fit the ATTN-to-D5 jumper on the Notecarrier CX header (§5), then connect the LiPo (or USB for bench testing). The device samples every 5 minutes, queues summaries, and sends them to Notehub every 30 minutes. The first env_summary.qo Notes should arrive in Notehub within 30 minutes of first power-on.
  4. Set thresholds: In Notehub, go to your Fleet → Environment Variables and set pm25_alert_ug_m3, pm10_alert_ug_m3, and db_a_alert (see §6). Alerts fire as soon as a threshold is crossed.

What you'll have: A solar-powered cellular area monitor that streams PM2.5, PM10, and sound levels as templated Notes to Notehub, with a real-time alert whenever a threshold is crossed.

Here's a sample Note from this device:

{
  "file": "env_summary.qo",
  "body": {
    "pm25_avg": 18.3,
    "pm25_peak": 42.7,
    "pm10_avg": 31.1,
    "pm10_peak": 89.4,
    "pm_samples": 6,
    "db_a_avg": 71.2,
    "db_a_peak": 83.6,
    "samples": 6,
    "voltage": 3.91,
    "lat": 37.774,
    "lon": -122.419,
    "location_valid": true
  }
}

4. Hardware Requirements

PartQtyTypeRationale
Notecarrier CX1RequiredIntegrated carrier with an embedded STM32L433 host, LiPo JST connector, Qwiic I²C port, and 6-channel ADC header, so you don't need a separate MCU. The host sleeps in STOP2 between samples, and the Notecard's ATTN pin wakes it.
Notecard Cell+WiFi (MBGLW) / datasheet1RequiredWith cellular, no site has to supply WiFi. The MBGLW also has built-in GNSS, so it can geo-stamp every reading without a separate GPS module.
Blues Flexible LTE, WiFi, or GPS/GNSS Antenna2RequiredOne goes on the MBGLW MAIN (cellular) u.FL port and one on the GPS u.FL port. The same Molex-made Blues flexible antenna works for both, since its 698 MHz–4.0 GHz broadband range includes GPS L1 at 1575 MHz. The MBGLW also has a WIFI u.FL port, which stays unconnected in this design because cellular is the only wireless path and the WiFi radio isn't used. The Hammond 1554CGY enclosure is ABS plastic (RF-transparent), so both flex antennas mount inside the enclosure on interior surfaces, with no cable glands or external mounts needed.
Blues Mojo1Optional (bench only)Coulomb counter for checking energy use on the bench. It confirms that sleep current, active sampling current, and per-session transmit energy match the expected figures. You don't need it for field deployment.
Adafruit PMSA003I Air Quality Breakout (#4632)1RequiredI²C optical particle counter that reports PM2.5 and PM10 in µg/m³. The Adafruit breakout has an onboard 5V boost circuit, so it runs correctly from the Notecarrier CX's 3.3V Qwiic rail.
DFRobot Gravity Analog Sound Level Meter (SEN0232)1RequiredA-weighted analog sound meter, 30–130 dB(A), 3.3–5 V supply, analog voltage output. The op-amp output connects straight to the host's 12-bit ADC with no extra circuitry.
SparkFun Sunny Buddy MPPT Solar Charger (PRT-12885)1RequiredMaximum power point tracking solar charger for a single-cell LiPo. It pulls as much current as it can from the panel as light conditions change, which matters a lot on a north-facing or partly shaded fence mount.
Adafruit 6V 6W Solar Panel (#1525)1RequiredWeatherproof monocrystalline panel, 930 mA peak. At 5 peak-sun hours per day, it generates well over the daily energy budget for this duty-cycled system. It mounts flat on the NEMA enclosure lid with the included screws.
Adafruit Lithium Ion Polymer Battery 3.7V 1200 mAh (#258)1RequiredSingle-cell LiPo with a 2-pin JST-PH connector. Check the real system draw with the Mojo bench exercise in §9 before you size the battery for deployment.
JST-PH 2-pin pigtail cable, female connector to bare-wire leads, ~100–150 mm (e.g., Adafruit #3814 or equivalent)1RequiredConnects the Sunny Buddy LOAD+/LOAD– through-hole pads to the Notecarrier CX LiPo JST receptacle. Solder the bare-wire end to the LOAD pads, then plug the JST-PH female connector into the Notecarrier's battery input.
Qwiic / STEMMA QT cable, 100 mm2RequiredOne connects the Notecarrier CX Qwiic port to the PMSA003I STEMMA QT connector for permanent field use. A second cable (optional, bench only) continues the daisy-chain from the PMSA003I's second STEMMA QT port to the Mojo's Qwiic port, so you can read the Mojo's LTC2959 coulomb counter over I²C during validation. See §5 bench wiring and §9.
NEMA 4X weatherproof enclosure, ~8×6×3″ (e.g., Hammond Manufacturing 1554CGY, ~9.4×6.0×3.1″, NEMA 4X ABS)1RequiredIP66-rated housing for outdoor mounting. Once you cut the sensor openings, it needs the port hardware listed below to stay weather-resistant.
IP-rated sintered-PE vent, M20 or M16 (e.g., Würth Elektronik 3800301 or equivalent)2RequiredLets air reach the PM2.5/PM10 sensor (inlet + exhaust) without giving up the enclosure's weather rating. Sintered PE passes particulate-laden air through for measurement but keeps bulk water out.
Weatherproof acoustic vent plug, 20–25 mm PTFE-membrane type (e.g., Würth Elektronik WE-AVE 3820101 or equivalent)1RequiredLets sound pressure waves reach the SEN0232 microphone capsule while sealing out rain and splash. Mount the board with its microphone aperture directly behind the vent opening. A bare hole doesn't keep the IP/NEMA weather resistance, even with foam packing.

All Blues parts ship with an active SIM that includes 500 MB of data and 10 years of service, with no activation fees and no monthly commitment.

5. Wiring and Assembly

Wiring diagram: PMSA003I on Qwiic, SEN0232 on A0 and V+, ATTN → D5 jumper so the Notecard can wake the host from STOP2, 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

The enclosure has to survive being bolted to a fence post in the rain and run on whatever sun the site gives it, so the build stays small. It's a Notecarrier CX with the Notecard Cell+WiFi (MBGLW) in its M.2 slot, two sensors (one on the I²C bus and one on the analog bus), one header jumper for host sleep, and a solar power chain on the back. All of the host I/O lands on the Notecarrier CX dual 16-pin headers. The MBGLW has three u.FL connectors: MAIN (cellular), GPS (GNSS), and WIFI. MAIN and GPS each take an antenna in this design. WIFI stays unconnected because cellular is the only wireless path (see antenna routing below).

Power chain (solar → LiPo → Notecarrier):

The Sunny Buddy is the only battery charger in this design. Its BAT JST port connects to the LiPo for charging, and the LOAD through-hole pads on its right edge feed the load (the Notecarrier CX system rail) from the same battery bus. You don't need a Y-splitter.

  1. Connect the solar panel's positive lead to the Sunny Buddy VIN+ screw terminal and the negative lead to VIN–.
  2. Plug the LiPo battery's JST-PH connector into the Sunny Buddy BAT port. The Sunny Buddy's MPPT circuit charges the battery through this connection, and it's the only charger in the loop.
  3. Wire the Sunny Buddy LOAD+ and LOAD– through-hole pads to the Notecarrier CX LiPo JST connector with a short JST-PH 2-pin pigtail cable (solder the pigtail's bare leads to the LOAD pads):
    • Bench with Mojo (inline power + Qwiic readback): Sunny Buddy LOAD → Mojo BAT input → Mojo LOAD output → Notecarrier CX LiPo JST connector. The Mojo sits inline on the power rail, and its LTC2959 coulomb counter keeps a running total of the charge drawn from the LiPo. To read that total in mAh, add a second 100 mm Qwiic cable from the PMSA003I's second STEMMA QT port to the Mojo's Qwiic port. That completes the daisy-chain: Notecarrier CX Qwiic → PMSA003I → Mojo. With Notecard firmware v8.1.3 or later, the Notecard detects the Mojo on the shared I²C bus by itself and periodically logs energy consumption data (including milliamp_hours) to _log.qo Notes. The Mojo documentation has the details. For a one-shot bench reading, send {"req":"card.power"} from the Notecard's in-browser serial terminal. The firmware in this project doesn't call card.power (see §10 for the extension path).
    • Deployed (without Mojo): Sunny Buddy LOAD → Notecarrier CX LiPo JST connector directly.
warning

USB-C conflict warning. While the Sunny Buddy LOAD is wired to the Notecarrier CX LiPo JST, do not connect USB-C to the Notecarrier at the same time. USB-C turns on the Notecarrier's onboard LiPo charger, which would then fight the Sunny Buddy for control of the battery. For USB-only bench sessions (programming, Notecard debug serial), disconnect the Sunny Buddy LOAD pigtail from the Notecarrier LiPo JST first, then connect USB-C by itself.

PM sensor (Adafruit PMSA003I, I²C):

  • Connect a 100 mm Qwiic cable from the Notecarrier CX's Qwiic port to the PMSA003I's STEMMA QT connector. The Qwiic bus carries 3.3V power, GND, SDA, and SCL in one connector, and the PMSA003I's onboard boost circuit takes care of its internal 5V requirement.
  • The PMSA003I needs a clear airflow path to sample ambient air. Mount it near the inlet vent on the enclosure wall, with its sensing aperture facing the vent opening. Cut a second opening on the opposite or adjacent wall for exhaust, because the sensor's internal fan creates a pressure differential that needs both an inlet and an outlet. Fit both openings with IP-rated sintered-PE vent plugs (see BOM) to keep the enclosure's weather rating. Put the inlet on a sheltered face (bottom or side, not the lid) so it gets as little direct rain as possible.

Sound level sensor (DFRobot SEN0232, analog):

  • VCC → Notecarrier CX V+ header pin (raw LiPo voltage, ~3.7–4.2 V). The SEN0232 is rated for 3.3–5V operation, and running it from V+ instead of 3.3V gives the op-amp more output headroom, especially for high-SPL readings near the 130 dB ceiling.
  • GND → Notecarrier CX GND header pin.
  • OUT → Notecarrier CX A0 header pin (ADC input).
  • The SEN0232 is a PCB module with an onboard microphone capsule, so there's no separate remote microphone cable. Mount the whole SEN0232 board inside the enclosure with its microphone aperture directly behind the acoustic vent opening. Cut a 20–25 mm diameter hole on the most sheltered face of the enclosure and fit it with a weatherproof acoustic vent plug (PTFE-membrane type, see BOM). Position the SEN0232 so its microphone port faces the vent membrane with a 2–5 mm gap. The PTFE membrane lets sound pressure waves through while sealing out moisture. A bare port or foam packing will compromise the NEMA 4X weather rating.

ATTN-to-D5 jumper (host sleep):

  • Fit a short jumper between the ATTN and D5 pins on the Notecarrier CX header. Both are on the same 16-pin header (ATTN at position 3, D5 at position 14). The Notecard pulls ATTN low while the host sleeps and raises it when the next sample is due, and the rising edge on D5 wakes the host from STOP2. Leave the Notecarrier CX EN pin unconnected. On the CX it enables the shared 3.3 V VIO rail (host, Qwiic, 3V3_OUT, and Notecard I/O), so driving it from ATTN would brown out the whole board instead of putting the host to sleep. Putting a Host to Sleep Between Sensor Readings walks through the full pattern.
note

Enclosure weather rating note. Every opening you cut for sensor airflow or sound transmission must be fitted with the IP-rated vent components listed in the BOM. Without them, the enclosure is de-rated to IP54 at best at the sensor ports. The sintered-PE inlet/exhaust vents and the PTFE acoustic membrane vent keep the enclosure's original weather rating at each opening.

Antenna routing:

The Hammond 1554CGY enclosure is ABS plastic, which is RF-transparent, so cellular and GPS signals pass through it without meaningful attenuation. Mount both flexible antennas inside the enclosure with their adhesive backing:

  • Peel the adhesive backing and press the cellular antenna flat against any interior wall surface, with the u.FL pigtail routed neatly back to the MBGLW's MAIN connector. Keep at least 10 mm (ideally 20 mm) of clearance between the antenna trace and any metal hardware or PCB ground planes.
  • Press the GNSS antenna flat against the interior of the top face of the enclosure (the upward-facing surface when the unit is deployed upright on a fence post or T-bar stake). The lid carries the solar panel on its outside, and the panel's glass, backing, and frame aren't RF-transparent. They'll attenuate or detune the GNSS antenna if it sits directly behind the panel. The top face is unobstructed and gives the antenna a clear RF path to the sky. Plug the pigtail into the MBGLW's GPS connector.
  • Leave the MBGLW's WIFI u.FL port unconnected, since this design doesn't use the WiFi radio.
note

Metal enclosure substitution. If you swap the ABS enclosure for steel or aluminum, the antennas have to exit the box. Metal fully blocks both cellular and GPS signals inside, so route the u.FL pigtails through IP-rated cable glands to externally mounted antennas.

6. Notehub Setup

  1. Create a project. Sign up at notehub.io and create a project. Copy the ProductUID and paste it into the PRODUCT_UID constant in the firmware (around line 22 of construction_env_monitor_helpers.h).

  2. Claim the Notecard. Power up the enclosure. On its first cellular session, the Notecard associates itself with your project using the ProductUID, and the device shows up on the Notehub dashboard within 1–2 minutes.

  3. Create a Fleet per site. Fleets (and Smart Fleets) group devices for shared configuration and routing. One fleet per active construction site is a natural unit. Every sensor on that site shares the same threshold environment variables, and you can tune them to whatever trades are working there this week. Archive the fleet when a site closes, and create a fresh one when a new site opens. To add the device to a fleet, go to Notehub dashboard → Devices → select your device → assign to Fleet.

  4. Set environment variables. Every variable below is optional, and the table shows the firmware defaults. To set them, go to Notehub dashboard → Fleets → select your fleet → Settings → Environment Variables → Add. Any value you push through Notehub overrides the firmware default on the device's next inbound sync, so you can tighten or relax thresholds without a firmware update or a truck roll to the site. The device picks up environment changes within 30 minutes. To force an immediate sync, send {"req":"hub.sync"} from the Notecard's in-browser serial terminal (Notehub → Devices → select device → In-Browser Serial). The firmware clamps each value to the range in the table, so an out-of-range value is pulled to the nearest limit. A value that isn't a number reads as 0 and gets the same treatment.

    VariableDefaultPurpose
    sample_interval_sec300Seconds between PM + sound samples. Range 60–3600.
    report_interval_min30Minutes between env_summary.qo Notes. If you change it, the firmware re-issues hub.set on the next wake to update the Notecard's outbound sync cadence. Range 5–1440.
    pm25_alert_ug_m335.0PM2.5 threshold (µg/m³) at or above which pm25_high fires. Range 5–500.
    pm10_alert_ug_m3150.0PM10 threshold (µg/m³) at or above which pm10_high fires. Range 5–1000.
    db_a_alert85.0dB(A) threshold at or above which db_a_high fires. Range 60–130.
    gps_interval_sec14400Seconds between GPS re-acquisition attempts (default 4 h). If you change it, the firmware re-issues card.location.mode on the next wake. Range 3600–86400.
    db_cal_offset0.0Signed dB bias added to all sound readings for field calibration against a reference meter. Range −20 to 20.
  5. Configure routes. Add a route that sends env_alert.qo to a real-time safety notification channel (email, Slack, an SMS gateway, or a CMMS ticketing endpoint). Add a second route that sends env_summary.qo to a long-term analytics store or compliance archive. Because the two Notefiles are separate at the source, the routes never need filter logic. Just point each Notefile at its destination.

7. Firmware Design

The firmware spends most of its time asleep, so the interesting code is the wake cycle. It samples sound first and PM second, folds both into a rolling window, checks the three alert thresholds, and decides whether to send an alert now or wait for the next 30-minute summary. Notecard config, environment-variable refresh, and GNSS handling all run around that core loop.

Build and Flash

Prerequisites:

  • Arduino IDE 2.0+ with Arduino core for STM32 installed, or arduino-cli v0.21+.
  • Required libraries:
    • Blues Wireless Notecard: arduino-cli lib install "Blues Wireless Notecard"
    • Adafruit PM25 AQI Sensor: arduino-cli lib install "Adafruit PM25 AQI Sensor"
    • STM32duino Low Power (and its dependency STM32duino RTC): arduino-cli lib install "STM32duino Low Power" "STM32duino RTC". This provides the STOP2 entry and ATTN-pin wake used in cx_sleep.h.

Build settings: The Notecarrier CX's host is the same STM32L433 as the Blues Cygnet, so select Blues boards → Cygnet (pnum=CYGNET) and set USB support (if available) to None (usb=none). If the USB CDC stack is enabled and no USB host is attached (battery power, or the Notecarrier CX USB-C switch in NC), the STM32's USB wakeup interrupt fires continuously and the host never stays in STOP2. Debug output (when DEBUG_SERIAL is defined in construction_env_monitor_helpers.h) goes to the LPUART on the Notecarrier CX debug jack instead, which an ST-LINK V3 exposes as a virtual COM port (see Serial Logging With STLINK).

Using Arduino IDE: Open firmware/construction_env_monitor/construction_env_monitor.ino, apply the build settings above, update PRODUCT_UID, and click Upload. The IDE picks up sketch.yaml automatically when you open the sketch directory, so the board options are already pinned to the correct variant.

Using arduino-cli:

arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/construction_env_monitor/
arduino-cli upload -p /dev/ttyACM0 --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/construction_env_monitor/

(Set -p to your serial port, for example COM3 on Windows or /dev/cu.usbmodem* on macOS. The board options above are the canonical match for the STM32L433 host on the Notecarrier CX. Don't substitute the older Nucleo_L433RC_P or generic GenL0 board variants, which don't match the host's chip family or peripheral mapping.)

Source Files

  • construction_env_monitor.ino holds the global state definitions, setup(), and loop().
  • construction_env_monitor_helpers.h has the shared constants, the AppState struct, extern declarations, and function prototypes.
  • construction_env_monitor_helpers.cpp has the sensor helpers, Notecard config helpers, note-send helpers, and sleepUntilAttn.
  • cx_sleep.h handles host sleep: the card.attn request, STOP2 entry, and ATTN wake.

Modules

ResponsibilityWhere
Power-up Notecard configuration (initial hub.set, from setup())notecardConfigure
Note template registration (idempotent, every wake)defineTemplates
Environment-variable fetch + range clamp per wakefetchEnvOverrides
Outbound-cadence / GPS-cadence re-apply when env vars changeapplyCardConfig (called from loop() every wake)
GPS position refresh from card.locationupdateGPS
PM2.5/PM10 averaging from PMSA003IreadPmSensor
Sound level averaging from SEN0232 ADCreadSoundLevelDb
Rolling window accumulation, threshold checks, alert emissionrunOneSampleCycle, sendAlert
Periodic exposure summarysendSummary
Host sleep (STOP2 until the Notecard raises ATTN)sleepUntilAttn → cx_sleep.h: cxSleepBegin, cxSleepUntilAttn
Application state (held in RAM across sleep cycles)AppState

Sensor reading strategy

Sound level is sampled first. setup() calls analogReadResolution(12) once to enable the host's 12-bit ADC. The SEN0232's output voltage maps linearly to dB(A). At the very start of the measurement phase, before begin_I2C() is called on the PMSA003I, the firmware samples at 4 Hz for 15 seconds (60 samples). Taking the dB(A) window before the PM sensor's 30-second warm-up begins keeps the warm-up itself out of the sound measurement. Be aware, though, that the host sleeping in STOP2 doesn't switch off the Qwiic 3.3 V rail. The PMSA003I stays powered between wakes, so its fan runs continuously and its noise floor is in the measurement no matter what order the sensors are sampled in. For complete, guaranteed acoustic isolation, add a GPIO-controlled load switch to the PMSA003I power line so the fan is definitely off during the sound window. See Limitations.

For each of the 60 ADC samples, the firmware maps the output voltage to dB(A) with the SEN0232's linear Vout-to-dB transfer function, applies the db_cal_offset calibration bias, converts the result to acoustic energy (10^(dB/10)), and adds it to a running sum. At the end of the 15-second window it converts the mean energy back to dB(A) with 10·log10(meanEnergy), which gives the acoustically correct mean sound pressure level. Averaging in the energy domain matters. Take a brief 100 dB event, which carries 100× more intensity than an 80 dB background. The simple arithmetic mean of the two dB values is 90 dB, while the energy-domain mean correctly gives ~97 dB. The 15-second window also smooths the noise floor naturally, damping single-spike artifacts from vehicles or tools passing the sensor.

The PM sensor is sampled after the sound window. The PMSA003I needs a 30-second warm-up delay (PM_WARMUP_MS) for its fan and laser to stabilize before its readings are valid. The Qwiic/3.3V rail stays on while the host sleeps in STOP2, so the PMSA003I isn't power-cycled between samples unless you add a load switch (see Limitations). The firmware always applies the full PM_WARMUP_MS delay after every successful begin_I2C() call, so readings are valid whether the sensor cold-started or was already running. After warm-up it takes 10 consecutive readings at 1 Hz and averages them. data.pm25_standard and data.pm10_standard (standard-atmosphere-corrected values) give a stable, comparable PM metric for trend monitoring. They're more consistent than the _env variants, which apply an extra humidity-based environmental correction. Neither output is suitable for silica-compliance reporting (see Limitations). Use the Mojo to measure the real idle current floor while the host sleeps (§9). Expect it to sit above the always-on SEN0232 quiescent draw by the amount of the PMSA003I's continuous fan current.

Event payload design

This design uses two template-backed Notefiles. Templates store records on the Notecard as fixed-length binary instead of free-form JSON, which shrinks the on-wire payload by roughly 3–5×.

pm25_avg and pm10_avg only average the wake cycles where the PM sensor returned a valid read (counted separately in AppState.pmSampleCount), so one failed read doesn't dilute the window mean. pm_samples carries that valid-read count as an explicit validity field. When pm_samples is 0, every read in the window failed, and pm25_avg and pm10_avg go out as –9999.0. That's an unambiguous invalid-data sentinel that no downstream consumer can mistake for genuinely particulate-free air. db_a_avg covers all wake cycles (sampleCount), since the ADC is always sampled. Here's an example summary body:

{
  "file": "env_summary.qo",
  "body": {
    "pm25_avg": 18.3,
    "pm25_peak": 42.7,
    "pm10_avg": 31.1,
    "pm10_peak": 89.4,
    "pm_samples": 6,
    "db_a_avg": 71.2,
    "db_a_peak": 83.6,
    "samples": 6,
    "voltage": 3.91,
    "lat": 37.774,
    "lon": -122.419,
    "location_valid": true
  }
}

And an example alert body:

{
  "file": "env_alert.qo",
  "body": {
    "alert": "pm25_high",
    "value": 48.2,
    "threshold": 35.0,
    "lat": 37.774,
    "lon": -122.419,
    "location_valid": true
  }
}

Notehub appends where_lat, where_lon, where_location, and where_timezone to every event as well, from the Notecard's last-known GNSS fix. The explicit lat/lon fields in the payload body are a belt-and-suspenders backup, since they survive even if a downstream route transformation strips the Notehub metadata. location_valid stays false until the GNSS confirms a new fix at the current site (the GPS time timestamp advances since boot). Downstream consumers should hold off on geo-stamping records or triggering geo-fenced alerts until this field is true. In a normal outdoor deployment, location_valid turns true within one or two 5-minute sample cycles, once the GNSS gets a fresh position.

Low-power strategy

Even on a solar-powered site, keeping the host asleep between samples cuts the average current draw a lot and keeps the enclosure cooler in direct sun. After a sample cycle, loop() calls sleepUntilAttn(), which hands off to cxSleepUntilAttn() (in cx_sleep.h). That function sends a card.attn sleep request for the trimmed interval, waits for the Notecard to pull ATTN low, and puts the STM32 host into STOP2. There it draws a few microamps with its RAM intact, so the rolling window accumulators, countdown timers, and last GPS fix stay in AppState with no serialization. When the interval is up, the Notecard raises ATTN, the rising edge on D5 wakes the host, and loop() runs the next cycle. setup() only runs once per power-up. Between cellular sessions the Notecard itself idles at ~8–18 µA @ 5V, which is close to nothing. The host is awake for about 55 seconds of each 5-minute cycle (~18% duty cycle), and the cellular session fires once per 30-minute report window.

Sampling and transmitting run on separate cadences on purpose. The firmware samples every 5 minutes but only transmits once every 30 minutes. Real data use depends on sync cadence, signal conditions, Notehub routing behavior, and how often alerts fire, so check it against Notehub's usage dashboard once the device is deployed in its target environment.

Retry and error handling

  • After notecardConfigure() sends the first hub.set, applyCardConfig() re-issues hub.set with requestAndResponse, because state.lastReportMin starts at 0 at power-up. That guarantees a response-checked request confirms the outbound cadence, even if notecardConfigure() hit a transient failure. Every hub.set from applyCardConfig() includes product, so the device can recover from a failed first-boot provisioning on any later wake without a hard reset.
  • readPmSensor() counts valid reads on its own and returns false if it gets zero valid readings (for example, when the sensor isn't detected on I²C). The calling code in runOneSampleCycle() makes sure a -1.0 reading never gets added to the accumulators.
  • The firmware NULL-checks env.get and card.location responses before using them and quietly skips a failed response, so a transient I²C error on one wake can't corrupt the application state.
  • A 30-minute cooldown per alert type (ALERT_COOLDOWN_SEC) de-duplicates alerts, so a sustained high-dust or high-noise condition doesn't produce a constant stream of them. One alert per type every 30 minutes is enough to notify the safety officer without flooding their inbox.
  • If the sleep request fails or ATTN never goes low (the ATTN→D5 jumper is missing), cxSleepUntilAttn() returns false. sleepUntilAttn() then waits out the trimmed interval awake with delay(), and loop() runs the next cycle as normal. A unit without the jumper still takes repeated periodic readings. It just doesn't get the power savings.

Key code snippet 1 — template definition

The firmware registers both templates on every wake. The call is idempotent, so a registration that fails on one wake is retried on the next. 14.1 is the note-c TFLOAT32 hint (a 4-byte IEEE 754 float), and 22 is TUINT16 (a 2-byte unsigned int). You declare a Boolean field by passing the literal true, because there's no numeric type hint for TBOOL. The note-c template data-type table lists the rest. This excerpt from defineTemplates() registers env_summary.qo (NOTEFILE_SUMMARY in construction_env_monitor_helpers.h). A failed registration is only logged, and the next wake tries again. env_alert.qo follows the same pattern on port 51.

J *req = notecard.newRequest("note.template");
if (!req) {
    // ...
} else {
    JAddStringToObject(req, "file", NOTEFILE_SUMMARY);
    JAddNumberToObject(req, "port", 50);
    J *body = JAddObjectToObject(req, "body");
    JAddNumberToObject(body, "pm25_avg",       14.1);  // 4-byte float
    JAddNumberToObject(body, "pm25_peak",      14.1);
    JAddNumberToObject(body, "pm10_avg",       14.1);
    JAddNumberToObject(body, "pm10_peak",      14.1);
    JAddNumberToObject(body, "db_a_avg",       14.1);
    JAddNumberToObject(body, "db_a_peak",      14.1);
    JAddNumberToObject(body, "samples",         22);   // 2-byte unsigned int
    JAddNumberToObject(body, "pm_samples",      22);   // 0 = all PM reads failed
    JAddNumberToObject(body, "voltage",        14.1);
    JAddNumberToObject(body, "lat",            14.1);
    JAddNumberToObject(body, "lon",            14.1);
    // ...
    JAddBoolToObject(body,   "location_valid",  true);
    J *rsp = notecard.requestAndResponse(req);
    if (rsp == NULL || notecard.responseError(rsp)) {
        // ...
    }
    if (rsp) notecard.deleteResponse(rsp);
}

Key code snippet 2 — immediate-sync alert

sync:true tells the Notecard to skip the outbound window and open a cellular session right now. That's how a dust spike gets off the device within seconds of the sample that catches it. full:true opts the Note out of the omitempty rule that templated Notefiles otherwise apply on Notehub serialization. Without it, location_valid: false (the explicit "GPS not yet confirmed at this site" signal) and any zero-valued lat/lon would be silently stripped from the body that downstream consumers receive. The env_summary.qo note.add sets the same full:true flag so that pm_samples: 0 survives serialization too. That's the companion sentinel to pm25_avg/pm10_avg = -9999.0 when every PM read in the window failed.

This is sendAlert(type, value, threshold). runOneSampleCycle() calls it with the alert name and the reading that tripped it, for example sendAlert("pm25_high", pm25, cfgPm25Alert), and arms that alert's 30-minute cooldown only when it returns true.

J *req = notecard.newRequest("note.add");
// ...
JAddStringToObject(req, "file", NOTEFILE_ALERT);
JAddBoolToObject(req,   "sync", true);
// ...
JAddBoolToObject(req,   "full", true);
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "alert",          type);
JAddNumberToObject(body, "value",          value);
JAddNumberToObject(body, "threshold",      threshold);
JAddNumberToObject(body, "lat",            state.siteLat);
JAddNumberToObject(body, "lon",            state.siteLon);
JAddBoolToObject(body,   "location_valid", state.gpsBootConfirmed);

J *rsp = notecard.requestAndResponse(req);
bool ok = (rsp != NULL) && !notecard.responseError(rsp);

Key code snippet 3 — sleep between samples

cxSleepUntilAttn() asks the Notecard to hold ATTN low for sleepSec seconds, confirms the pin has gone low, and puts the STM32 into STOP2. The ATTN rising edge on D5 wakes it, and loop() carries on. Since the host stays alive, the firmware checks the result of the card.attn request instead of treating it as a fire-and-forget command. The sleep duration is the configured sample interval minus however long the host was already awake this cycle, which keeps each sample starting on the configured cadence. setup() calls cxSleepBegin() once to arm the ATTN wake, and loop() ends each cycle with sleepUntilAttn(sleepSec), shown here. log is the debug serial port, or NULL when DEBUG_SERIAL isn't defined.

void sleepUntilAttn(uint32_t sleepSec) {
    // ...
    if (!cxSleepUntilAttn(notecard, sleepSec, NULL, log)) {
        // The Notecard didn't take the sleep request, or ATTN never went low
        // (check the ATTN -> D5 jumper).  Keep the sample cadence and try again.
        // ...
        delay(sleepSec * 1000UL);
    }
}

8. Data Flow

Data flow: 5-minute sample cycle (sound first, then PM) → rolling window accumulation → three alert rules (env_alert.qo sync:true) and 30-minute summary (env_summary.qo templated) → Notehub routes

Every 5 minutes (by default), the host wakes and reads both sensors, sound first and then PM. It adds the readings to its sample statistics and checks the three alert thresholds, sending an immediate alert if one trips. Then it checks the report timer, sends a summary if one is due, and goes back to sleep.

Collected. PM2.5 and PM10 in µg/m³ (standard atmospheric), mean dB(A) over a 15-second audio window, the peak PM2.5, PM10, and dB(A) values seen in the current reporting window, battery voltage, and GPS lat/lon.

Transmitted.

  • env_summary.qo: once per report_interval_min (2 Notes per hour by default, one every 30 minutes). These are template-encoded, queued on the Notecard, and flushed by the periodic outbound session. The PM averages (pm25_avg, pm10_avg) only cover valid PM-sensor reads, and pm_samples is the count of those reads. When pm_samples is 0, every PM read in the window failed and pm25_avg/pm10_avg carry –9999.0, an unambiguous invalid-data sentinel. db_a_avg is the mean over all wake cycles, and samples records the total number of measurement cycles in the window.
  • env_alert.qo: only on a threshold breach, with sync:true and a 30-minute cooldown per alert type. The alert types are pm25_high, pm10_high, and db_a_high.

Routed. Both Notefiles arrive at Notehub, and separate routes send them on from there: env_alert.qo to a real-time notification channel and env_summary.qo to a long-term compliance archive or analytics dashboard.

What triggers alerts.

  • pm25_high: a PM2.5 reading at or above pm25_alert_ug_m3 (default 35 µg/m³). A sustained reading above 35 µg/m³ is worth acting on during a cutting or grinding operation.
  • pm10_high: a PM10 reading at or above pm10_alert_ug_m3 (default 150 µg/m³). The 150 µg/m³ default is a heuristic starting point for flagging elevated coarse-particulate conditions. It is not equivalent to a regulatory limit or violation. The EPA 24-hour NAAQS for PM10 applies to a 24-hour average, so a single 5-minute sample above that number is not an exceedance. Adjust the threshold to match how much dust the specific tasks on your site generate.
  • db_a_high: the mean dB(A) over the 15-second window is at or above db_a_alert (default 85 dB(A)). The 85 dB(A) default is a heuristic starting point, not an OSHA compliance determination. OSHA's noise action level applies to an 8-hour TWA integrated over a full shift. This device measures a 15-second mean, and the two are not directly comparable. Use the alert as an early-warning prompt to investigate, not as documentation of a regulatory exceedance.

9. Validation and Testing

Expected steady-state cadence. On a quiet morning, a freshly deployed unit should produce two env_summary.qo events per hour (one every 30 minutes at default settings) and no env_alert.qo events. On a working construction site, expect the occasional db_a_high alert when heavy equipment runs near the sensor and the occasional pm25_high alert during active cutting, grinding, or sweeping.

Notehub verification. After first power-on, the device claims itself to the project on its first cellular session (~1–2 minutes). Your Notecard should show up on the Devices page of the Notehub dashboard with a device name, which defaults to the Notecard's serial number. The first env_summary.qo Note appears in the Events table within 30 minutes. Click it to inspect the JSON payload. Check that pm25_avg, pm10_avg, and db_a_avg are present and non-zero, and that lat/lon are non-zero if the device has a view of the sky for GPS. location_valid: false on the first few summaries is normal. It turns true once the GNSS gets a fresh fix at the current site.

A two-phase first-light sanity check.

Phase 1: USB bench (boot and I²C validation). Power the Notecarrier CX from USB-C with no LiPo connected, and connect an ST-LINK to the debug jack for the firmware's debug output. The build has USB CDC disabled so the host can sleep, which means there's no USB serial port. setup() runs once. Then loop() runs one full sample cycle, including the 30-second PM warm-up, and sleeps in STOP2 until the Notecard raises ATTN. That repeats indefinitely with no re-flashing. (Without the ATTN→D5 jumper, the firmware waits out each interval awake instead, which is fine for a quick I²C check.) Open the ST-LINK serial port (115200 baud) to watch the debug output. You should see Notecard I²C init, the hub.set, note.template, and env.get requests, PM warm-up and sensor reads, and sound-level ADC samples. By default you will not see a summary note.add after the first cycle, because the report countdown starts at 1800 seconds (30 minutes). After about six cycles (~30 minutes of wall-clock time), the countdown fires and an env_summary.qo Note is queued. To check the alert path, (a) set pm25_alert_ug_m3 to 5, the lowest value the firmware accepts, in the Notehub Fleet Environment Variables, (b) wait for the device's next inbound sync (~30 minutes by default, or force one with {"req":"hub.sync"} in the in-browser serial terminal), and (c) watch the next sample cycle fire pm25_high right away. Reset the variable to 35.0 afterward.

Phase 2: LiPo/solar (repeated-sampling validation). Disconnect USB-C. Wire the Sunny Buddy LOAD to the Notecarrier CX LiPo JST and connect a charged LiPo (see §5 power chain). Make sure the ATTN→D5 jumper is fitted (§5), since that's what lets the Notecard wake the host from STOP2. The host now wakes every sample_interval_sec (5 minutes by default), runs one full loop() sample cycle, and sleeps. env_summary.qo should show up in Notehub about 30 minutes after first power-on (six 5-minute cycles). If the countdown fires but no Note appears, check hub.status in the blues.dev In-Browser Terminal for cellular registration errors.

Simulating an alert. Drop pm25_alert_ug_m3 to 5 in the Fleet environment variables (lower values are clamped up to 5 µg/m³). The next inbound sync pulls the updated value, the next sample cycle fires pm25_high as long as the air holds at least 5 µg/m³ of PM2.5, and the alert should reach Notehub once a cellular session is established (typically 15–60 seconds). Reset the variable to 35.0 when you're done testing.

Using Mojo to validate power behavior. Notecard's published idle figure is ~8–18 µA @ 5V between cellular sessions (the low-power design guide has the authoritative measured numbers). Active cellular transmit averages ~250 mA, and the STM32L433 host in STOP2 adds roughly 1–2 µA. The SEN0232 runs from the V+ header pin (raw LiPo voltage), which stays on while the host sleeps, so the sensor's op-amp circuitry draws a continuous quiescent current. Measure that floor directly with the Mojo to characterize your specific build.

The Mojo sits inline between the Sunny Buddy LOAD output and the Notecarrier CX LiPo JST connector. Its LTC2959 coulomb counter (Analog Devices) keeps a running total of the charge drawn from the LiPo whether or not anything is connected to its Qwiic port. To read that total in mAh, connect the second Qwiic cable from the PMSA003I's second STEMMA QT port to the Mojo's Qwiic port (the bench daisy-chain described in §5). With Notecard firmware v8.1.3 or later, the Notecard detects the Mojo on the shared I²C bus by itself and periodically logs energy data (including milliamp_hours) to _log.qo Notes. For a one-shot bench reading, you can also send {"req":"card.power"} from the Notecard's in-browser serial terminal. The firmware in this project doesn't call card.power, but the §10 extension note describes how to add it to the summary payload.

Here's a useful bench exercise. Leave the unit running on a fully charged 1200 mAh LiPo for 24 hours with no solar input, then note the Mojo tally. Only the Notecard's published figures are vendor-backed, so measure the other phase currents instead of assuming them:

PhaseCurrentDuration per 5-min cycle
Notecard idle + host in STOP2 (between syncs)~10–20 µA @ 5V (Notecard ~8–18 µA published by Blues; host ~1–2 µA)~4 minutes
Notecard cellular sync (~once per 30 minutes)~250 mA avg (published by Blues)~30 sec
SEN0232 quiescent on V+ (always-on)Measure with Mojoentire cycle
dB(A) sample window (SEN0232 + host)Measure with Mojo~15 sec
PM warm-up + sample (host + PMSA003I)Measure with Mojo~40 sec
GNSS re-acquisition (~once per 4 h)Measure with Mojovaries with sky view

The Mojo trace should have a recognizable shape. The Qwiic/3.3V rail stays on while the host sleeps in STOP2, so the sleep floor is the always-on SEN0232 quiescent draw plus the PMSA003I's continuous fan current. On top of that floor you'll see a brief step for the 15-second dB(A) sample window, then a larger ~40-second step for PMSA003I warm-up and reads, and a ~30-second cellular-sync pulse every 30 minutes. If you add the load switch described in §10, the floor should drop to the SEN0232 draw alone. Use the Mojo to confirm that, because the measured floor shows directly whether the PM sensor is sleeping or running, and that decides whether the power budget and the acoustic isolation hold up. If the trace instead stays elevated at 10–80 mA with no sleep pattern, the host isn't entering STOP2. Check the ATTN→D5 jumper and confirm the firmware was built with the usb=none board option. Before you size the battery for deployment, confirm the trace across at least one full 4-hour GNSS re-acquisition cycle.

10. Limitations and Next Steps

This reference design is deliberately an area monitor. It is not a personal dosimeter or a regulatory silica sampler, and it isn't a complete replacement for either one. The limits below come with that choice, so a GC evaluating the design can see exactly where it complements existing compliance work and where a production deployment will need to bring more. After the limits comes the work that would turn this into a full site monitoring program.

Simplified for the POC

These limits come from building an area monitor rather than a personal dosimeter. Each one shows where the design fits alongside existing compliance work and what a production deployment will have to add.

PM sensor fan acoustic contamination. The PMSA003I's fan runs whenever its Qwiic 3.3V supply is present, and that supply stays on while the host sleeps in STOP2. As built, then, the fan runs continuously and its noise floor is in the dB(A) channel for every measurement cycle. The firmware still samples sound before begin_I2C() and the PM warm-up phase, so the warm-up itself never overlaps the sound window. Before deploying, measure the enclosure's self-noise floor with the PM sensor powered and spinning, using a calibrated reference sound-level meter placed at the acoustic vent opening. If the enclosure self-noise floor exceeds 5 dB(A) above the ambient floor, the enclosure design is contaminating the dB(A) channel. The definitive fix is a GPIO-controlled load switch on the PMSA003I's Qwiic power line, so the fan is definitely off for the entire sound-sample window. A load switch also takes the PMSA003I fan current out of the sleep floor, which makes the power budget unambiguous.

5-minute sampling cadence can miss transient events. The firmware wakes and samples once per sample_interval_sec (5 minutes by default). A high-dust burst or noise spike that starts and ends entirely between two consecutive sample windows isn't captured, because the sensor never sees it. During task-intensive work like concrete demolition, jackhammering, dry cutting, or grinding, consider dropping sample_interval_sec to 60–120 seconds with the Notehub environment variable to narrow that detection gap. More frequent sampling reduces LiPo autonomy proportionally and uses more data. For tasks where any transient spike matters (a saw that only runs for 30 seconds, for example), a wearable personal dosimeter worn by the worker is the only reliable way to capture it. The area monitor cannot substitute for it in those scenarios.

PM sensor ≠ direct silica monitor. The PMSA003I is an optical particle counter that measures total PM2.5 and PM10 by mass concentration. It can't speciate particles, so it can't tell crystalline silica from wood dust, cement dust, or urban background particulate. OSHA's silica standard (29 CFR 1926.1153) requires respirable crystalline silica sampling with NIOSH Method 7500 or an equivalent, which uses filter-based personal samplers analyzed by X-ray diffraction. This device's PM2.5/PM10 readings are a real-time proxy indicator for elevated dust conditions, not a substitute for OSHA-compliant silica exposure documentation. The alerts can prompt a safety officer to investigate and deploy personal monitoring, but they cannot replace it.

Sound level is 15-second mean, not 8-hour TWA. OSHA's noise standard uses the TWA (time-weighted average) over a full work shift, integrated by a dosimeter that accounts for the whole exposure curve, quiet and loud periods alike. This device reports a 15-second moving average every 5 minutes and flags when that average exceeds 85 dB(A). That helps you find high-noise task windows, but it is not a substitute for a personal dosimeter worn by a worker for the whole shift.

SEN0232 calibration. The DFRobot SEN0232 is rated for 3.3–5V operation, so powering it from the LiPo's V+ rail (~3.7–4.2V) is within spec. The sensor's published output mapping (0.6V → 30 dB, 2.6V → 130 dB) is factory-calibrated at 5V, though, and at lower supply voltages its readings may differ from a calibrated reference by a fixed offset. At commissioning time, use the db_cal_offset environment variable to null that offset against a calibrated reference sound-level meter.

Single-point perimeter monitoring. One sensor reflects conditions at one location on the site. Exposure varies a lot by trade, task, and distance from the source, and a concrete saw 3 meters from the sensor reads very differently from one 20 meters away. A production deployment would spread multiple sensors across the site's active work zones.

Alert cooldown prevents rapid re-notification. The 30-minute per-type alert cooldown stops alarm fatigue, but it also means an elevated exposure that lasts for hours generates only two alerts per hour. For compliance documentation, the env_summary.qo stream is the authoritative record, not the alert count.

PM sensor failure emits –9999.0 as an explicit sentinel. If the PMSA003I isn't detected on I²C for an entire reporting window, pm25_avg and pm10_avg go out as –9999.0. No downstream consumer can mistake that unambiguous invalid-data sentinel for genuinely particulate-free air (0.0 µg/m³). The companion pm_samples field confirms total failure when it reads 0. If pm_samples = 0 persists across several windows in a row, look for a sensor wiring, I²C, or power fault.

GPS coordinates may be absent or stale until location_valid is true. Two different situations can produce coordinates you shouldn't trust. The first is no fix acquired yet (zero coordinates). If the Notecard hasn't gotten any GPS fix before the first summary goes out (say, the enclosure was set up briefly indoors), siteLat and siteLon are both 0.0 in the payload body. Notehub's where_lat/where_lon metadata will be absent too, and both stay zero or absent until the first GNSS acquisition. The second is a stale cached fix after redeployment. The Notecard's GNSS hardware keeps its last-known position across power cycles. Move the device from one construction site to another, power it on fresh, and card.location may immediately return the previous site's coordinates, a non-zero and apparently valid position that's geographically wrong for the new site. The firmware guards against this by comparing GPS time timestamps. It treats the first non-zero response after boot as a baseline and holds location_valid at false. Only when a later card.location response returns a newer timestamp, confirming that the GNSS got a fresh fix at the current site, does it set location_valid to true and arm gpsCountdown for the normal re-acquisition interval. In both cases, location_valid: false in the Note body is the signal. Downstream consumers should not geo-stamp compliance records or evaluate geo-fenced alert rules on Notes where location_valid is false. In a typical outdoor deployment with a clear view of the sky, the GNSS gets a fresh fix within one or two sample cycles (~5–10 minutes). After that, location_valid turns true and stays true across sleep/wake cycles for the rest of the current site deployment.

Mojo is bench-validation only. The firmware doesn't call card.power to read the Mojo's LTC2959 coulomb counter. Adding a mojo_mah field to the summary is a straightforward extension. Include a card.power request in sendSummary() and append the returned milliamp_hours value to the queued Note body.

Production Next Steps

Turning this into a full site-monitoring program takes the work below. It runs roughly from site coverage, through fleet maintenance, to power sizing.

Multi-sensor deployments spread coverage across the site. Put two to four units on each active site, for example one downwind of the main dust-generating task (usually the saw or grinder), one on the site perimeter as a perimeter check, and one upwind near the site office as a baseline. Keep every device on the same per-site fleet so they share site-wide threshold environment variables, and use device-level environment variable overrides for any per-unit calibration differences (like db_cal_offset).

Shift-aware sampling cadence matches data volume to activity. Use the Notecard's on-device time (from card.time) to sample less often outside work hours (9 PM to 5 AM, for example) and extend sampling during peak-activity windows like concrete operations or demolition. That cuts data volume and battery load during the hours when no workers are on site.

Personal dosimeter integration complements the area monitor. Add a second, wearable Notecard + Notecarrier CX variant as a personal dosimeter worn by a worker, with the site monitor acting as the reference station. Comparing personal and ambient readings lets the GC show that workers in a specific trade zone had lower exposure than the perimeter sensor suggested.

Field-upgradeable firmware through Notecard Outboard DFU lets you push threshold changes, new alert types, or calibration-constant updates to the entire fleet from Notehub without a site visit.

Extended battery for winter/cloudy deployments keeps the monitor alive through low-sun stretches. Once the Mojo bench exercise (§9) has established the real weighted-average system draw for your build, size the battery to match. At northern latitudes in December, you may get only 2–3 peak-sun hours a day. A 3000–5000 mAh LiPo pack and a higher-wattage panel (10W) give you several cloudy days of autonomy and recover faster when the sun comes back. Before you finalize the battery size, confirm it against at least one full 4-hour GNSS re-acquisition cycle in the Mojo trace.

11. Summary

The safety officer who used to wait until the shift ended to see yesterday's PM2.5 and dB(A) numbers now gets a geo-stamped reading every 30 minutes, plus an alert as soon as a 5-minute sample catches a threshold breach. That's long before the cut is finished, the saw is shut down, and the dust has settled. The GC doesn't have to negotiate WiFi access with a subcontractor, run cable to a temporary power outlet, or wait until morning to find out what yesterday's concrete cutting looked like. One Notecarrier CX, one cellular Notecard, one solar panel, and the same firmware deploy identically on every site in the GC's portfolio, with one fleet per site for threshold tuning. When the site moves in three months, the enclosure moves with it. The summary stream archives the area-monitoring record. The alert stream closes the real gap, because it lets the safety officer redirect workers, deploy PPE, and dispatch personal monitoring before the shift ends instead of after.

On This Page

  • 1. Project Overview
  • 2. System Architecture
  • 3. Technical Summary
  • 4. Hardware Requirements
  • 5. Wiring and Assembly
  • 6. Notehub Setup
  • 7. Firmware Design
    • Build and Flash
    • Source Files
    • Modules
    • Sensor reading strategy
    • Event payload design
    • Low-power strategy
    • Retry and error handling
    • Key code snippet 1 — template definition
    • Key code snippet 2 — immediate-sync alert
    • Key code snippet 3 — sleep between samples
  • 8. Data Flow
  • 9. Validation and Testing
  • 10. Limitations and Next Steps
    • Simplified for the POC
    • Production Next Steps
  • 11. Summary

Resources

  • GitHub
  • Request More Info
Can we improve this page? Send us feedback
© 2026 Blues Inc.
© 2026 Blues Inc.
AboutDocsAPI ReferenceTermsPrivacy