Construction Site Environmental & Noise Exposure Monitor

This reference application is here to give you ideas and help you get started quickly. It's built on specific hardware that may not match yours, so focus on the sections that fit your use case. If you'd like to talk through your project and whether it's a good fit for Blues, reach out.
Building with an AI assistant? Point it at this accelerator's firmware in firmware/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
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
- Clone and configure: Download this repo and open
firmware/construction_env_monitor/construction_env_monitor_helpers.h. Replace thePRODUCT_UIDconstant with your Notehub project UID (see §6 step 1). - 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 matchfirmware/construction_env_monitor/sketch.yaml, so you can also leave out--fqbnwhen you run from the sketch directory. Theusb=noneboard 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/ - 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.qoNotes should arrive in Notehub within 30 minutes of first power-on. - Set thresholds: In Notehub, go to your Fleet → Environment Variables and
set
pm25_alert_ug_m3,pm10_alert_ug_m3, anddb_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
| Part | Qty | Type | Rationale |
|---|---|---|---|
| Notecarrier CX | 1 | Required | Integrated 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) / datasheet | 1 | Required | With 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 Antenna | 2 | Required | One 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 Mojo | 1 | Optional (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) | 1 | Required | I²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) | 1 | Required | A-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) | 1 | Required | Maximum 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) | 1 | Required | Weatherproof 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) | 1 | Required | Single-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) | 1 | Required | Connects the Sunny Buddy LOAD+/LOAD– through-hole pads to the Notecarrier CX LiPo JST receptacle. Solder the bare-wire end to the LOAD pads, then plug the JST-PH female connector into the Notecarrier's battery input. |
| Qwiic / STEMMA QT cable, 100 mm | 2 | Required | One connects the Notecarrier CX Qwiic port to the PMSA003I STEMMA QT connector for permanent field use. A second cable (optional, bench only) continues the daisy-chain from the PMSA003I's second STEMMA QT port to the Mojo's Qwiic port, 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) | 1 | Required | IP66-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) | 2 | Required | Lets 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) | 1 | Required | Lets 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
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.
- Connect the solar panel's positive lead to the Sunny Buddy
VIN+screw terminal and the negative lead toVIN–. - Plug the LiPo battery's JST-PH connector into the Sunny Buddy
BATport. The Sunny Buddy's MPPT circuit charges the battery through this connection, and it's the only charger in the loop. - Wire the Sunny Buddy
LOAD+andLOAD–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→ MojoBATinput → MojoLOADoutput → 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 (includingmilliamp_hours) to_log.qoNotes. 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 callcard.power(see §10 for the extension path). - Deployed (without Mojo): Sunny Buddy
LOAD→ Notecarrier CX LiPo JST connector directly.
- Bench with Mojo (inline power + Qwiic readback): Sunny Buddy
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 fromV+instead of 3.3V gives the op-amp more output headroom, especially for high-SPL readings near the 130 dB ceiling. - GND → Notecarrier CX
GNDheader pin. - OUT → Notecarrier CX
A0header 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 (
ATTNat position 3,D5at position 14). The Notecard pullsATTNlow while the host sleeps and raises it when the next sample is due, and the rising edge onD5wakes the host from STOP2. Leave the Notecarrier CX EN pin unconnected. On the CX it enables the shared 3.3 VVIOrail (host, Qwiic,3V3_OUT, and Notecard I/O), so driving it fromATTNwould brown out the whole board instead of putting the host to sleep. Putting a Host to Sleep Between Sensor Readings walks through the full pattern.
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.
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
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID and paste it into the
PRODUCT_UIDconstant in the firmware (around line 22 ofconstruction_env_monitor_helpers.h). -
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.
-
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.
-
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.Variable Default Purpose sample_interval_sec300Seconds between PM + sound samples. Range 60–3600. report_interval_min30Minutes between env_summary.qoNotes. If you change it, the firmware re-issueshub.seton 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_highfires. Range 5–500.pm10_alert_ug_m3150.0PM10 threshold (µg/m³) at or above which pm10_highfires. Range 5–1000.db_a_alert85.0dB(A) threshold at or above which db_a_highfires. Range 60–130.gps_interval_sec14400Seconds between GPS re-acquisition attempts (default 4 h). If you change it, the firmware re-issues card.location.modeon 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. -
Configure routes. Add a route that sends
env_alert.qoto a real-time safety notification channel (email, Slack, an SMS gateway, or a CMMS ticketing endpoint). Add a second route that sendsenv_summary.qoto 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-cliv0.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 dependencySTM32duino RTC):arduino-cli lib install "STM32duino Low Power" "STM32duino RTC". This provides the STOP2 entry and ATTN-pin wake used incx_sleep.h.
Build settings: The Notecarrier CX's host is the same STM32L433 as the
Blues Cygnet, so select Blues boards → Cygnet (pnum=CYGNET) and set USB
support (if available) to None (usb=none). If the USB CDC stack is
enabled and no USB host is attached (battery power, or the Notecarrier CX USB-C
switch in NC), the STM32's USB wakeup interrupt fires continuously and the
host never stays in STOP2. Debug output (when 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.inoholds the global state definitions,setup(), andloop().construction_env_monitor_helpers.hhas the shared constants, theAppStatestruct, extern declarations, and function prototypes.construction_env_monitor_helpers.cpphas the sensor helpers, Notecard config helpers, note-send helpers, andsleepUntilAttn.cx_sleep.hhandles host sleep: thecard.attnrequest, STOP2 entry, and ATTN wake.
Modules
| Responsibility | Where |
|---|---|
Power-up Notecard configuration (initial hub.set, from setup()) | notecardConfigure |
| Note template registration (idempotent, every wake) | defineTemplates |
| Environment-variable fetch + range clamp per wake | fetchEnvOverrides |
| Outbound-cadence / GPS-cadence re-apply when env vars change | applyCardConfig (called from loop() every wake) |
GPS position refresh from card.location | updateGPS |
| PM2.5/PM10 averaging from PMSA003I | readPmSensor |
| Sound level averaging from SEN0232 ADC | readSoundLevelDb |
| Rolling window accumulation, threshold checks, alert emission | runOneSampleCycle, sendAlert |
| Periodic exposure summary | sendSummary |
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 firsthub.set,applyCardConfig()re-issueshub.setwithrequestAndResponse, becausestate.lastReportMinstarts at 0 at power-up. That guarantees a response-checked request confirms the outbound cadence, even ifnotecardConfigure()hit a transient failure. Everyhub.setfromapplyCardConfig()includesproduct, 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 returnsfalseif it gets zero valid readings (for example, when the sensor isn't detected on I²C). The calling code inrunOneSampleCycle()makes sure a -1.0 reading never gets added to the accumulators.- The firmware NULL-checks
env.getandcard.locationresponses 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
ATTNnever goes low (theATTN→D5jumper is missing),cxSleepUntilAttn()returnsfalse.sleepUntilAttn()then waits out the trimmed interval awake withdelay(), andloop()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
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 perreport_interval_min(2 Notes per hour by default, one every 30 minutes). These are template-encoded, queued on the Notecard, and flushed by the periodicoutboundsession. The PM averages (pm25_avg,pm10_avg) only cover valid PM-sensor reads, andpm_samplesis the count of those reads. Whenpm_samplesis0, every PM read in the window failed andpm25_avg/pm10_avgcarry–9999.0, an unambiguous invalid-data sentinel.db_a_avgis the mean over all wake cycles, andsamplesrecords the total number of measurement cycles in the window.env_alert.qo: only on a threshold breach, withsync:trueand a 30-minute cooldown per alert type. The alert types arepm25_high,pm10_high, anddb_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 abovepm25_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 abovepm10_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 abovedb_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:
| Phase | Current | Duration 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 Mojo | entire 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 Mojo | varies 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.