Lone Worker Panic and Fall Detection Beacon
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/lone_worker_beacon/ so it can learn how the device works and help you adapt it to your hardware.
This project is a wearable safety assurance device for utility linemen, oilfield pumpers, field service technicians, and solo contractors. A single Notecard for Skylo carries cellular, Skylo NTN satellite, and WiFi radios on one module and fails over between them automatically. Put it in a belt-clip enclosure and you have a cellular-first, satellite-backed distress beacon. It detects falls, accepts an explicit panic-button press, and can reach a dispatcher from the middle of nowhere, which is exactly where lone-worker incidents happen.
1. Project Overview
Not a certified life-safety device. This is a proof-of-concept reference design meant to demonstrate Blues hardware and firmware patterns. It is not a certified personal emergency response system, it is not classified as a man-down or lone-worker protection device under any regulatory scheme, and it has not been evaluated for fail-safe or safety-critical operation. It must supplement, not replace, established lone-worker safety procedures, mandatory check-in protocols, required PPE, and any regulatory or contractual safety obligations that apply to your operation. Never deploy this design as a sole means of worker protection.
The problem. A utility lineman working an isolated substation, an oilfield pumper checking a remote wellhead at night, and a field-service tech in a basement boiler room at 2 AM all share the same vulnerability. If something goes wrong, nobody will know for hours. Lone-worker incidents don't always announce themselves, and falls from elevation and heart events are both silent. The worker just stops moving, and no one notices until a shift check-in is missed or a buddy makes a welfare call.
Most safety-conscious operations already require check-in procedures, so awareness isn't the gap. Those procedures depend on the worker remembering to push a button every 30 minutes, and what's missing is automatic, continuous monitoring that doesn't. These workers need a device that watches for the physical signatures of an incident (sudden free-fall, violent deceleration on impact, prolonged motionlessness after a fall) and raises an alarm without the worker doing anything. The panic button is a secondary escape valve. It's an explicit human override for situations where the physics don't look like a fall but the worker knows something is very wrong.
This project is that device. It's a wearable safety beacon with two core
detection modes, automatic fall detection and explicit panic-button input. The
onboard STM32L433 host runs a two-stage fall-detection algorithm on the LIS3DH
accelerometer, watches a held-down panic button with debounce logic, and drives
a haptic motor to acknowledge every confirmed event. The accelerometer is
sampled at ~100 Hz. A 10-sample inner loop runs at 10-millisecond intervals so
free-fall phases as short as 80 milliseconds are always observed, while Notecard
I/O and state checks run at the outer ~10 Hz cadence (Section
7 covers the sampling design). On a fall or panic, the
firmware immediately queues a compact emergency Note carrying the Notecard's
cached location and sends it with sync:true, so the alert never waits on GPS.
A non-blocking GPS search then runs in the background without pausing fall or
button monitoring. If a fresh fix arrives within the timeout window, a follow-up
beacon_location.qo Note is queued with the event-time coordinates. See
Section 7 for the full two-Note flow.
Why Notecard. You can't take cellular coverage for granted where lone-worker incidents happen. A substation at the edge of a service area, a gas compressor station in a rural county, and a mine portal are exactly the places where a worker is most isolated and where cellular signal is most likely to be marginal or absent. Relying on cellular alone means dangerously assuming the signal will be there when it's needed most.
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.
That's why the whole design is built around Notecard for Skylo (NOTE-NBGLWX) instead of treating satellite as a nice-to-have bolted on later. One M.2 module carries three radios: cellular (LTE-M / NB-IoT / GPRS), satellite over the Skylo non-terrestrial network (NTN), and WiFi. It picks among them automatically. The cellular path covers the vast majority of activations, since cellular is widely deployed even in surprisingly rural areas. When cellular really does fail, the Skylo satellite link is right there on the same board, with no companion module or second device to wire in. Skylo covers supported regions (the Notecard for Skylo datasheet shows the current coverage footprint). Inside that footprint, a device with an unobstructed view of the sky can get a distress message to the Blues Notehub cloud service even when every terrestrial network is unavailable. Satellite coverage is not guaranteed in every no-cellular location. It depends on the Skylo coverage region, sky-view geometry, and antenna orientation. For the substations, oilfields, and rural worksites this design targets, though, it provides the safety margin that cellular alone cannot.
The firmware sets a single
card.transport
preference of wifi-cell-ntn. The Notecard prefers WiFi wherever a provisioned
AP is reachable, falls back to cellular (the de facto primary for a roaming
worker), and falls back again to Skylo satellite when every terrestrial network
is unavailable. Failover happens inside the Notecard, and the host firmware
never branches on which network is live. The WiFi path is an opportunistic
bonus. A field tech standing near a facility WiFi AP might sync an alert without
using any cellular at all, but that path is only active when network
credentials have been provisioned on the Notecard and a compatible AP is in
range.
Deployment scenario. The beacon ships as a self-contained unit in a rugged
belt-clip enclosure, powered by a 3.7V LiPo battery. Workers clip it onto a belt
or hard-hat band like a pager. Each device's worker ID is pre-provisioned in
Notehub before deployment. A change to worker_id reaches the device on the
next inbound sync, which happens every 2 hours by default, so it doesn't take
effect right at shift start. Falls and panics generate immediate alerts. There's
no app to install and no phone to pair, and the worker doesn't have to pay any
attention to it. They just clip it on and go.
2. System Architecture
Device-side responsibilities. This device must never miss the moment
something goes wrong, so the STM32L433 host on the Notecarrier CX never sleeps.
Instead it runs a dual-cadence loop. A fast inner loop reads the LIS3DH every 10
milliseconds (matching the sensor's 100 Hz ODR), and an outer ~10 Hz cadence
handles Notecard I/O, GPS polling, the panic-button debounce, and the DRV2605L
haptic feedback. As soon as the two-stage algorithm confirms a fall, or the
worker holds the button, the host queues the alert Note with the Notecard's
cached location and triggers an immediate sync. It then starts a non-blocking
GPS search that runs in the background and never pauses fall detection. If a
fresh fix arrives within the window, a follow-up beacon_location.qo Note
carries the event-time coordinates. All Notecard communication stays on I²C, so
the firmware has no AT commands, serial framing, or session management to
babysit.
Notecard responsibilities. The Notecard holds the daily flush schedule via
hub.set but treats any
sync:true alert Note as an immediate interrupt, so the dispatcher hears about
a fall in the same minute it happens instead of at the next scheduled outbound
window. Location is the Notecard's job too. Once the host calls
card.location.mode, the Notecard caches each GPS fix and embeds it in every
later compact template Note via _lat/_lon, and the firmware never has to
pass coordinates in its Note body.
When the worker walks into a coverage hole and cellular fails, Notecard for Skylo quietly switches to its onboard Skylo satellite (NTN) radio and tries the satellite path. Neither the firmware nor Notehub can tell the difference. The same Note arrives with the same template and event structure, no matter which radio carried it.
Notehub responsibilities. Notehub ingests and stores
every Note from both transports and forwards events to whatever dispatch system
the operator has configured. beacon_alert.qo events and their paired
beacon_location.qo follow-ups land at the dispatcher's real-time endpoint.
Environment
variables
travel the other way on each inbound sync, so a safety supervisor can retune the
free-fall threshold or change a worker ID from the Notehub console without ever
opening an enclosure.
Routing to the cloud (high level). Notehub can route to HTTP, MQTT, AWS,
Azure, and several other destinations. Route setup depends on your project, and
this project doesn't ship a specific downstream endpoint, so see the Notehub
routing docs for
details. A typical deployment routes both beacon_alert.qo and
beacon_location.qo to the same real-time dispatch endpoint, joined by device
UID and event_id.
3. Technical Summary
What you'll have when you're done: a wearable beacon that detects falls and panic-button presses, sends alerts to Notehub over cellular or satellite, and gives haptic feedback on every event.
Prerequisites: Arduino IDE or arduino-cli, a Notehub account, and a
Notecarrier CX fully assembled following the wiring in Section
5.
Step 1: Install dependencies
# Install the STM32 core
arduino-cli core install STMicroelectronics:stm32
# Install required libraries
arduino-cli lib install "Blues Wireless Notecard" \
"Adafruit LIS3DH" \
"Adafruit DRV2605 Library" \
"Adafruit BusIO"Step 2: Configure and compile
- Open
firmware/lone_worker_beacon/lone_worker_beacon.inoin the Arduino IDE or a text editor. - In
lone_worker_beacon_helpers.h, find the line#define PRODUCT_UID ""and put your Notehub project's ProductUID between the quotes (you'll find it on notehub.io under project settings). - Compile (the board options below match
firmware/lone_worker_beacon/sketch.yaml, which the Arduino IDE picks up automatically when you open the sketch directory):
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET firmware/lone_worker_beacon/Step 3: Flash to Notecarrier CX
arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET \
-p /dev/ttyUSB0 firmware/lone_worker_beacon/(Replace /dev/ttyUSB0 with your platform's serial port, such as COM3 on
Windows. You can find the port in the Arduino IDE's Tools menu.)
Step 4: Test and verify
- Power the beacon. The Notecard claims itself to your project on its first sync.
- Hold the panic button for 2+ seconds. You should feel a triple haptic buzz (alert queued).
- Check Notehub. Go to your project's Devices tab, select your device, and open
the Events log. A
beacon_alert.qoevent with"type":"panic"should show up within 30–90 seconds. - Drop the beacon 50–80 cm onto a padded surface. You should feel a double buzz
(fall detected) and see a
beacon_alert.qowith"type":"fall"in the Events log.
If no event appears, check Section 10 (Troubleshooting).
Here's a sample Note from this device:
{
"uid": "d84e3a...",
"device": "dev:000000000000000",
"file": "beacon_alert.qo",
"received": 1714582020,
"best_lat": 40.71280,
"best_lon": -74.00601,
"best_location": "New York NY",
"body": {
"type": "fall",
"worker_id": "lineman-042",
"event_id": 7,
"voltage": 3.71,
"loc_age_s": 142
}
}4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an embedded STM32L433 host, so you don't need a separate MCU. Its I²C, SPI, analog, and GPIO headers support the full sensor stack. |
| Notecard for Skylo (NOTE-NBGLWX) (datasheet) | 1 | One M.2 module carrying cellular (LTE-M / NB-IoT / GPRS, Quectel BG95-S5 modem), Skylo satellite (NTN), and WiFi (Silicon Labs WFM200S) radios, plus integrated GPS/GNSS. It seats in the Notecarrier CX's M.2 slot. The firmware's card.transport wifi-cell-ntn setting makes it prefer WiFi, fall back to cellular, and fall back again to the Skylo satellite network when every terrestrial network is unavailable. Failover is automatic, with no companion module or second device. Requires the antennas below. |
| Skylo-certified LTE/satellite antenna included with Notecard for Skylo (u.FL) | 1 | Connects to the MAIN u.FL port and carries both the terrestrial cellular signal and the Skylo satellite link, so one antenna serves both networks. Use only the Skylo-certified antenna supplied with Notecard for Skylo. Substituting an uncertified antenna risks regulatory non-compliance and link failure. A belt-worn beacon needs this antenna where it can see the sky, so position it against the top (sky-facing) wall of the polycarbonate enclosure. Polycarbonate is RF-transparent, so no external routing or bulkhead is required. In the northern hemisphere, a southward orientation improves Skylo link margin. The same placement that enables satellite fallback works for cellular too. |
| Passive GPS/GNSS antenna (u.FL) per the Notecard for Skylo datasheet | 1 | Connects to the GPS u.FL port for the Notecard's own GNSS time/location. This is the device's location source, and card.location draws on it for the coordinates embedded in alert Notes. Stick it next to the main antenna on the top (sky-facing) interior wall of the polycarbonate enclosure for the best acquisition geometry during GPS-on events. |
| Blues Mojo | 1 | Coulomb counter on the LiPo rail that gives you ground-truth current and energy measurements during bench validation. |
| SparkFun LIS3DH Triple Axis Accelerometer Breakout (SEN-13963) | 1 | 3-axis MEMS accelerometer at 0x18 on I²C, configured at 100 Hz ODR (a 10 millisecond hardware sample period). The firmware samples at ~100 Hz through a 10-sample inner loop (one read every 10 milliseconds), which matches the sensor ODR and reliably catches free-fall phases as short as 80 ms. The ±4g range leaves headroom for both normal impacts and genuine falls. Its built-in free-fall and shock interrupt hardware is a production upgrade path. |
| Adafruit DRV2605L Haptic Motor Controller (#2305) | 1 | I²C haptic driver with 123 built-in waveform effects. It drives the ERM motor directly, with no transistor or PWM circuit needed, and supports both ERM and LRA motors. |
| Adafruit Vibrating Mini Motor Disc (#1201) | 1 | Small flat ERM disc motor. It gives distinct, wrist-perceptible confirmation buzzes on fall and panic events and wires directly to the DRV2605L output terminals. |
| SparkFun Momentary Push Button Switch, 12mm Square (COM-09190) | 1 | Panic input, wired to D9 with firmware INPUT_PULLUP. For field use, choose a cap that can be operated with a gloved hand. |
| 3.7V LiPo battery, 1200 mAh, JST-PH 2-pin (e.g. Adafruit #258) | 1 | Powers the device. Runtime depends on alert frequency and transport conditions, so validate it with Mojo before you size for deployment. Use a cell with built-in protection circuitry. |
| Adafruit JST PH 2-Pin Cable, Female Connector 100mm (#261) | 1 | Adapts the LiPo cell's JST-PH male plug to bare wire leads for connection to the Mojo BAT+ and GND solder pads. You need it to complete the LiPo→Mojo power path without cutting or modifying the LiPo cell's factory connector. |
| Polycarbonate project enclosure, ≥120 × 80 × 40 mm interior, IP54+ (e.g., Hammond 1553JGYBK) | 1 | Protects the board stack, battery, and connectors in a field-deployable package. The Notecarrier CX (83 × 63 mm) sets the minimum floor area. Must be polycarbonate (non-metal), because metal enclosures block cellular, satellite, WiFi, and GNSS signals. It needs cutouts for the panic button and for the LiPo JST connector (battery access). Notecard for Skylo's MAIN and GPS u.FL antennas stick to the interior walls with no external routing. IP54 or better is recommended for outdoor field use. Check the interior dimensions against your own board-and-battery stack before ordering. No charging circuit is included in this BOM. Section 11 explains why. Plan enclosure cutouts for any charging connector you add later. |
Notecard for Skylo ships with an active global SIM that includes 500 MB of cellular data and 10 years of service, plus 10 KB of bundled Skylo satellite data, with no activation fees, no monthly commitment, and no separate satellite provider subscription. Additional satellite data is billed per byte (see the Notecard for Skylo datasheet for current pricing).
Installer-supplied, deployment-specific item. You'll need a belt clip or other wearable mounting hardware to attach the beacon to a worker's belt, hard-hat band, or safety vest. Pick a clip rated for the enclosure's weight (~300 g fully loaded). Some enclosure families include an optional clip-arm accessory, or you can mount a spring-steel belt clip to the outside of the enclosure with M3 screws.
Charging and power access. This BOM and wiring include no LiPo charging circuit, dock, or power-switch hardware. The project runs on a bare LiPo cell until it's depleted. A production wearable needs a USB-C LiPo charging circuit built into the enclosure, plus overcharge and short-circuit protection if the cell's built-in circuitry doesn't already provide it. See Section 11 for details. Anyone moving from bench validation toward a field-deployed unit should expect to add a charging path next.
5. Wiring and Assembly
The Notecarrier CX, Notecard for Skylo, accelerometer, haptic driver, and panic
button all have to fit inside a belt-clip enclosure that a worker will forget
they're wearing. All of the host I/O lands on the Notecarrier
CX dual 16-pin headers.
Notecard for Skylo seats in the M.2 slot and talks to the STM32 host over the
carrier's internal I²C. Cellular, Skylo satellite, and WiFi all live on that one
module, so there's no companion satellite device to wire in, just the two u.FL
antennas described below. During bench validation, the Mojo sits inline between
the LiPo JST connector and the Notecarrier CX +VBAT pad.
Both I²C peripherals (LIS3DH and DRV2605L) share the SDA/SCL bus on the Notecarrier CX headers. The carrier provides onboard pull-ups, so the I²C lines don't need external resistors.
Pin-by-pin:
- +3V3 → LIS3DH
VIN, DRV2605LVIN(both are 3.3V-native, and the breakout datasheets give the exact voltage range). - GND → LIS3DH
GND, DRV2605LGND, one leg of the panic button. - SDA → LIS3DH
SDA, DRV2605LSDA. - SCL → LIS3DH
SCL, DRV2605LSCL. - D9 → other leg of the panic button (the firmware uses
INPUT_PULLUP, active-low). - DRV2605L
MOTOR+/MOTOR-→ Adafruit Vibrating Motor Disc red and blue wires (polarity matches the driver output, so swap them if the motor doesn't run). - Power path (LiPo → Mojo → Notecarrier CX). The Mojo sits inline on the
battery rail as a coulomb counter. Wire it as follows:
- Plug the JST-PH female pigtail (BOM item) onto the LiPo's JST-PH male
connector. This gives you two bare wire leads:
+(red) and−(black). - LiPo
+lead → MojoBAT+solder pad (or screw terminal markedBAT). - LiPo
−lead → MojoGNDsolder pad. The Mojo'sGNDterminal is common to both the battery-negative rail and the load-negative rail. - Mojo
LOAD+output → Notecarrier CX+VBATpad (completes the positive supply to the board stack). - Mojo
GNDterminal → Notecarrier CXGNDheader pin (this completes the ground-return path, and without it the circuit is open).
- Plug the JST-PH female pigtail (BOM item) onto the LiPo's JST-PH male
connector. This gives you two bare wire leads:
- Notecard for Skylo antennas →
MAINu.FL port → included Skylo-certified antenna (carries both cellular and satellite);GPSu.FL port → passive GPS/GNSS antenna. Both stick to the top (sky-facing) interior wall of the polycarbonate enclosure (see the antenna note below). There's no companion satellite module or JST cable, because all three radios are on Notecard for Skylo itself.
Antenna placement and connector mapping. Notecard for Skylo uses two u.FL
ports. Connect the included Skylo-certified antenna to the MAIN port. It
carries both the cellular signal and the Skylo satellite link, so one
antenna serves both networks. Connect the passive GPS/GNSS antenna to the GPS
port. Stick both elements to the top (sky-facing) interior wall of the
polycarbonate enclosure. Polycarbonate is RF-transparent, so you don't need
external pigtail routing or a bulkhead. A belt-worn beacon only reaches the
Skylo satellite network when its MAIN antenna can see the sky, so make sure
that wall faces up when the unit is worn. In the northern hemisphere, a
southward orientation improves Skylo link margin. Use only the Skylo-certified
antenna supplied with Notecard for Skylo on the MAIN port, because
substituting an uncertified antenna risks regulatory non-compliance and link
failure. The GPS port is the location source for all card.location data
embedded in alert Notes.
Charging is out of scope for this POC. This build has no charging circuit, dock, or inductive coil wired in. Route the LiPo JST connector to an accessible spot on the enclosure wall so you can swap the battery or connect a bench charger without fully taking the unit apart. Do not seal the JST connector inside the enclosure with no external access path.
The LIS3DH's SDO/SA0 pin sets its I²C address. Leave it unconnected or pull it to GND for address 0x18 (the firmware default). The DRV2605L's address (0x5A) is fixed, so the two don't conflict.
6. Notehub Setup
Detailed walkthrough for first-time users:
-
Create a project. Sign up at notehub.io. Click Create a Project → name it (e.g., "Lone Worker Beacons") → select your region → confirm. Copy the ProductUID shown on the project details card and paste it into
firmware/lone_worker_beacon/lone_worker_beacon_helpers.has thePRODUCT_UIDmacro value before flashing. -
Claim the Notecard. Power the beacon with a Notecard and provisioned SIM in place. On its first cellular or WiFi session, the Notecard associates itself with your project. To check, go to Devices in Notehub and click your device's UID. It should show up within 30–90 seconds.
-
Complete the initial non-NTN sync. Before any Skylo satellite (NTN) transmission is possible, Notecard for Skylo must complete at least one successful cellular or WiFi sync to associate with your project and register the compact template definitions in Notehub. The firmware's
card.transportwifi-cell-ntnsetting does that first sync over cellular/WiFi automatically. Commission each beacon where it has terrestrial coverage, even if it will routinely operate over satellite, and confirm a sync lands in Notehub before you deploy it. Thecard.transportAPI reference has the details. -
Create a Fleet per region or team. Fleets group devices for shared configuration. One fleet per crew or site is a natural structure. Every device in a fleet shares the same detection thresholds, and each device gets its own worker ID override. If workers cross territories, Smart Fleets can automatically move a device to a different fleet based on its reported location.
-
Set environment variables. Every variable below is optional, and the firmware defaults apply until you override them. To set one in Notehub, open your project → Devices tab → click your device → Environment → Fleet Environment (applies to all devices in the fleet) or Device Environment (overrides fleet settings for this device alone) → add key-value pairs. Values reach the Notecard's local cache on the next inbound sync (every 2 hours by default) and then the host on the following
env.getpoll (also up to 2 hours), so a change can take up to 4 hours to land. You don't need to re-flash. Enter each threshold as a plain number, such as0.45or300. The whole value has to be a number, so a typo like2,5or300msis ignored rather than read as 2 or 300. The_mssettings also have to be whole numbers. Any rejected or out-of-range value is ignored, and the current setting stays in place. Deleting a variable doesn't reset it either. The device keeps the last value it accepted until it reboots.Variable Default Purpose worker_idworker-001Human-readable worker or device identifier included in every Note. Maximum 24 characters. The device silently truncates longer values to keep the compact-packet payload small. Pre-provision this per device before deployment. Changes reach the device on the next inbound sync (up to 2 hours by default), not immediately at shift start. freefall_g0.55Total acceleration magnitude (g) below which the firmware declares a free-fall phase. Values outside 0.10–0.90 g are ignored. impact_g2.5Total acceleration magnitude (g) above which fall impact is confirmed. Values outside 1.50–8.00 g are ignored. fall_window_ms500Milliseconds after a free-fall episode during which an impact must be detected to confirm the fall. Values outside 100–2000 ms are ignored. freefall_min_ms80Minimum milliseconds the device must stay in free-fall before the impact window opens. Shorter free-falls (stumbles, tool drops) are ignored. Values outside 20–500 ms are ignored. panic_hold_ms2000Milliseconds the button must be held before a panic alert fires. Prevents accidental triggers from gloved hands. Values outside 500–10000 ms are ignored. -
Configure routes (optional for now, but you'll want them for live dispatch). Go to Routes → Create Route → name it (e.g., "Dispatch Alerts") → select Event → filter by file
beacon_alert.qo→ select a destination (HTTP, SMTP, Slack, etc.). Important: Add a second route forbeacon_location.qoto the same destination. When a fresh GPS fix arrives during the 90-second background search,beacon_location.qocarries event-time coordinates that supersede the cached location in the pairedbeacon_alert.qo. Downstream systems must pair the two Notes by(device, event_id), becauseevent_idresets to 0 on each power cycle and isn't globally unique on its own. The(device, event_id)join key stays unambiguous across repeated alerts, retries, and network reordering.
7. Firmware Design
The firmware is split into three small files so the safety-critical detection
paths stay easy to read. All three live in firmware/lone_worker_beacon/:
firmware/lone_worker_beacon/lone_worker_beacon.inoholds the constants, globals,setup(), andloop().firmware/lone_worker_beacon/lone_worker_beacon_helpers.hhas the shared#defineconstants,externdeclarations, env-var clamp ranges, and function prototypes.firmware/lone_worker_beacon/lone_worker_beacon_helpers.cpphas all the helper function implementations.
Dependencies:
- Arduino core for STM32
(
stm32duino/Arduino_Core_STM32), installed from the Arduino IDE Boards Manager. Blues Wireless Notecard(thenote-arduinolibrary). Install it from the Arduino Library Manager:arduino-cli lib install "Blues Wireless Notecard".Adafruit LIS3DHand itsAdafruit Unified Sensordependency. It drives the SparkFun breakout in the BOM over I²C. Install it from the Library Manager:arduino-cli lib install "Adafruit LIS3DH".Adafruit DRV2605 Libraryand itsAdafruit BusIOdependency. Install both from the Library Manager.
Modules
| Responsibility | Function |
|---|---|
Notecard configuration (hub.set + one-time card.transport wifi-cell-ntn for cellular→satellite fallback) at boot; returns fault state | notecardConfigure() |
Compact template registration (2 Notefile templates plus an env.template for satellite; returns fault state) | defineTemplates() |
| Environment variable refresh with clamp validation; called at boot and every 2 h | fetchEnvVars() |
| LIS3DH setup (100 Hz, ±4g) | initAccel() |
| DRV2605L setup (ERM library 1) | initHaptic() |
| Two-stage fall detection | pollFallDetection() |
| Hold-to-confirm panic button | checkPanicButton() |
| Start non-blocking GPS search after an alert | beginGpsSearch() |
Advance GPS search; queue beacon_location.qo on fresh fix | pollGpsSearch() |
Alert Note with immediate sync (cached location + loc_age_s + event_id) | sendAlert() |
| Arm non-blocking haptic pulse sequence | triggerHaptic() |
| Advance haptic state machine (called every loop pass) | pollHaptic() |
Fall Detection Algorithm
The firmware uses a two-stage software algorithm sampled at ~100 Hz. Each outer
loop pass makes ten pollFallDetection() calls, spaced 10 milliseconds apart in
a fast inner loop (matching the LIS3DH's 100 Hz ODR). At that rate the 80
millisecond minimum free-fall duration guard (DEFAULT_FREEFALL_MIN_MS) spans
~8 consecutive samples, which gives meaningful noise rejection. Notecard I/O,
GPS polling, and haptic state advance once per outer pass at the ~10 Hz outer
cadence. A single-stage threshold check that just watches for a spike produces
too many false positives from everyday bumps. Requiring a free-fall phase before
the impact check cuts down on nuisance alerts from walking into a doorframe or
dropping a tool.
Stage 1: Free-fall. On each of the 10 inner-loop reads (spaced 10
milliseconds apart), the firmware computes the total acceleration magnitude,
|a| = √(ax² + ay² + az²). When total-g drops below freefall_g (default
0.55g) and stays there for at least freefall_min_ms (default 80ms, remotely
configurable through Notehub), the firmware exits Stage 1 and opens an
impact-watch window. At ~100 Hz the 80 millisecond guard spans ~8 consecutive
readings, which is enough for meaningful noise rejection. A genuine free-fall
from ~30 cm bench height lasts well over 100 ms.
Stage 2: Impact. If total-g exceeds impact_g (default 2.5g) within
fall_window_ms (default 500ms) of the free-fall phase ending, the fall is
confirmed. If no impact arrives, the window closes on its own, so a brief
stumble or a tool being set down doesn't generate a false alert.
Event Payload Design
Both Notefiles use compact
templates
with "format":"compact". That's a hard requirement for Skylo NTN (satellite)
transport. Compact Notes use fixed-length binary encoding, which keeps the
satellite packet well inside the 256-byte maximum payload limit and keeps
per-message satellite cost as low as possible. The _lat/_lon keywords in the
template body tell the Notecard to embed its cached GPS location automatically,
so the firmware doesn't need to pass coordinates in the note.add body.
Both templates include an event_id field. It's a monotonic counter that goes
up once per new alert dispatch, not once per retry. The same event_id
value appears in both beacon_alert.qo and its paired beacon_location.qo.
Downstream systems must use (device, event_id) as the join key, because
event_id resets to 0 on each power cycle and isn't globally unique on its own.
The scoped (device, event_id) pair stays stable across repeated alerts of the
same type, across retries, and across network reordering.
Example beacon_alert.qo event as it appears in Notehub (initial alert, cached
location):
{
"uid": "d84e3a...",
"device": "dev:000000000000000",
"file": "beacon_alert.qo",
"received": 1714582020,
"best_lat": 40.71280,
"best_lon": -74.00601,
"best_location": "New York NY",
"body": {
"type": "fall",
"worker_id": "lineman-042",
"event_id": 7,
"voltage": 3.71,
"loc_age_s": 142
}
}Example beacon_location.qo follow-up event (fresh event-time fix, sent only if
GPS acquires within 90 seconds):
{
"uid": "d84e3b...",
"device": "dev:000000000000000",
"file": "beacon_location.qo",
"received": 1714582087,
"best_lat": 40.71294,
"best_lon": -74.00589,
"best_location": "New York NY",
"body": {
"type": "fall",
"worker_id": "lineman-042",
"event_id": 7
}
}The type field in beacon_alert.qo carries one of two values, fall (the
two-stage algorithm confirmed it) or panic (button held). loc_age_s is how
old the Notecard's cached GPS fix was, in seconds, when the alert was queued
(−1.0 if no fix was available). Since event_id resets to 0 on each power
cycle, downstream correlation should use (device, event_id) as the join key.
When a beacon_location.qo with a matching event_id is also present, its
coordinates supersede the cached location in beacon_alert.qo for mapping and
dispatch response.
Low-Power Strategy
This POC keeps the host MCU awake continuously, and that's a deliberate design
choice. The device has to detect a fall or button press at any moment, so it
can't use the timed-sleep pattern the other Notecarrier CX accelerators use. In
that pattern the host sits in STM32 STOP2 until the Notecard's ATTN pin wakes
it on a host GPIO via card.attn (the Notecarrier CX can't power-gate its host;
see Putting a Host to Sleep Between Sensor
Readings).
That's incompatible with the monitoring requirement, because the host would be
blind to the accelerometer until the Notecard's timer fired. So the host MCU
stays awake and runs a dual-cadence loop. The LIS3DH is sampled at ~100 Hz
through a 10-sample inner loop (10 milliseconds between reads), and
Notecard/GPS/haptic state advances at the outer ~10 Hz cadence. The inner-loop
delays pace the outer cadence without a separate top-level delay() call. The
battery-life penalty is real and substantial. The STM32L433 at its default clock
draws about 10–15 mA active, which makes host idle the dominant draw at steady
state. The power validation table has per-state
figures. Validate total runtime with Mojo before you size a battery for
deployment.
Notecard for Skylo runs in periodic mode with outbound: 1440 (daily flush)
and inbound: 120 (2-hour environment-variable refresh). notecardConfigure()
also issues a one-time card.transport wifi-cell-ntn, so the Notecard prefers
WiFi, then cellular, then Skylo satellite (NTN). The Notecard handles that
failover internally, with no firmware branching. Every emergency Note carries
sync:true, which bypasses the outbound interval and triggers an immediate
session over whichever radio is reachable. The daily flush is a backstop for
any queued data that sync:true didn't deliver. Between sessions the Notecard
sits in its own low-power idle state (~8 µA), whichever radio it last used. GPS
is off by default and only turns on during alert events (fall, panic).
Continuous GPS would draw an additional 30+ mA, and the design's event-driven
location updates don't need it.
A production implementation should use STM32L433 low-power STOP2 mode with GPIO wakeup on the LIS3DH hardware interrupt (INT1) and the button pin. That's the same STOP2-with-GPIO-wake mechanism the ATTN pattern relies on, with the sensor and button as the wake sources instead of the Notecard. It drops host idle to ~2–3 µA and still catches every fall event in real time. See Limitations.
Retry and Error Handling
Startup path. notecardConfigure() and defineTemplates() both return a
boolean. If either one returns false, setup() latches g_setupFault and fires
a distinctive slow double-buzz haptic pattern, so a misconfigured device is
obvious at power-on without needing debug serial. A missing or empty
PRODUCT_UID is also caught at runtime and added to the fault latch.
The firmware treats any startup fault as unrecoverable. That covers a missing or
empty PRODUCT_UID, a hub.set failure, any template registration failure,
and an LIS3DH initialization failure. After the slow double-buzz pattern fires,
setup() enters an infinite halt loop that repeats the fault buzz every 5
seconds. The beacon never enters loop(), and no alerts are ever sent. For a
safety device, that hard stop is intentional. Arming a beacon whose Notes can't
route, or whose compact Skylo NTN transport may be broken, gives a false sense
of protection. The recurring buzz makes an unconfigured device unmistakable at
power-on. Verify PRODUCT_UID, Notecard connectivity, and template
registration, then power-cycle before deploying.
Sensor degradation. initAccel() and initHaptic() each return a boolean,
and the main loop checks g_accelReady and g_hapticReady before calling the
relevant functions. A missing or unresponsive LIS3DH at boot is a hard fault.
g_setupFault is latched alongside g_accelFaultLatched, the distinctive slow
double-buzz fault pattern fires, and setup() enters the infinite halt loop.
That way a beacon that can't do fall detection is immediately obvious at
power-on and can't be silently deployed as panic-only. A missing DRV2605L just
skips haptic feedback without crashing the loop. An accelerometer fault that
shows up after boot (consecutive bad reads beyond ACCEL_FAIL_THRESHOLD after
ACCEL_REINIT_MAX failed reinitializations) latches g_accelFaultLatched and
clears g_accelReady, which permanently disables fall detection until a power
cycle. The device then buzzes once every 30 seconds so an operator can tell the
unit has dropped to degraded panic-only mode.
Notecard response errors. Every requestAndResponse() call checks for a
NULL response and calls notecard.responseError() before reading any fields.
A failed transaction is skipped, and the loop carries on with stale values.
When note.add fails for an alert, the non-blocking retry queue retries it up
to ALERT_RETRY_MAX (3) times, 500 milliseconds apart. Each retry carries the
original event_id and loc_age_s, so no context is lost.
Important: queueing failure vs. delivery failure. The Notecard only stores
and retries (for cellular or satellite delivery) the Notes that note.add
successfully accepts. If the firmware's retry budget runs out before
note.add succeeds, for example because the Notecard is temporarily
unreachable on I²C, the alert is dropped and is never delivered to Notehub.
The firmware logs this with DEBUG_PRINTLN but takes no further action. If you
need stronger guarantees, persist unsent alerts in non-volatile storage until
note.add succeeds, or trigger a local fault (e.g., a distinctive haptic
pattern) when the retry budget is exceeded. An operator who suspects a missed
alert should send {"req":"hub.sync.status"} from the blues.dev In-Browser
Terminal to check the last sync time, whether Notes are still waiting to sync,
and the last sync error.
GPS acquisition. The non-blocking GPS state machine polls card.location
every 2 seconds (throttled from the 10 Hz loop rate) for up to
DEFAULT_GPS_TIMEOUT_SEC (90 seconds). Before the alert Note is queued,
loop() saves the current cache epoch in a per-alert local variable
(thisCacheEpoch). It computes loc_age_s from that value and a card.time
call, then passes the result to sendAlert(). When no GPS search is already
running, thisCacheEpoch is also copied into g_gpsCacheEpoch, the freshness
baseline the search uses. A fix only counts if
its epoch is later than that baseline, so a stale cached fix can't be mistaken
for a fresh acquisition. On a fresh fix, a beacon_location.qo Note is queued
immediately with sync:true. The timeout check uses elapsed time
(millis() - start >= interval) instead of an absolute deadline, so it stays
correct across the 49.7-day millis() rollover. If no fresh fix arrives before
the timeout, GPS is turned off and only the initial beacon_alert.qo stands.
Only one GPS enrichment window can be active at a time. If a second alert fires
during the 90-second window (possible because the 60-second cooldown is shorter
than the GPS timeout), beginGpsSearch() returns immediately and the second
alert gets only its cached location, with no beacon_location.qo queued for
it.
Alert rate limiting and suppression. DEFAULT_ALERT_COOLDOWN_SEC (60
seconds) gates fall and panic alerts. If a fall or panic happens within 60
seconds of the previous alert, no Note is queued and no GPS acquisition is
attempted. A suppressed fall gives no local indication, while a suppressed panic
gives a single haptic buzz so the worker knows the hold registered. That single
buzz is different from the triple buzz that confirms an alert was accepted. The
cooldown keeps a tumbling device from causing an alert storm, and the worker
doesn't have to do anything.
Key Code Snippet 1: Compact Template with _lat/_lon
Skylo NTN (satellite) transport requires the format: "compact" and port
arguments. The _lat/_lon template fields tell the Notecard to embed its
cached best-available location (GNSS or cell-derived) in the compact packet
automatically, so the firmware never passes coordinates in the Note body. The
type and worker_id fields use the string hint "s". The event_id field
uses type hint 14 (4-byte signed int32, per the Blues compact-template
encoding table). It's written to both beacon_alert.qo and
beacon_location.qo with the same value, and downstream systems join the two
Notes using (device, event_id) as the key. This excerpt is from
defineTemplates(), which then registers beacon_location.qo on port 52 and
reports any failed registration back to setup() as a startup fault.
J *req = notecard.newRequest("note.template");
if (req) {
JAddStringToObject(req, "file", "beacon_alert.qo");
JAddNumberToObject(req, "port", 50);
JAddStringToObject(req, "format", "compact");
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "type", "s");
JAddStringToObject(body, "worker_id", "s");
JAddNumberToObject(body, "event_id", 14); // 4-byte signed int32 correlation key
JAddNumberToObject(body, "voltage", 14.1); // 4-byte float
JAddNumberToObject(body, "loc_age_s", 14.1); // seconds; -1.0 = unknown
JAddNumberToObject(body, "_lat", 14.1); // from Notecard GPS cache
JAddNumberToObject(body, "_lon", 14.1);
J *rsp = notecard.requestAndResponse(req);
ok = (rsp != NULL && !notecard.responseError(rsp));
if (rsp) notecard.deleteResponse(rsp);
}The same function also sends an
env.template.
Notecard for Skylo only receives environment variables over satellite when
they're templated, so this is what lets a threshold or worker_id change
reach a beacon that's working on Skylo. Every variable is templated as a
string, because fetchEnvVars() parses and range-checks the text itself. A
failed env.template is logged but isn't a startup fault, since alerts don't
depend on it and cellular or WiFi syncs deliver environment variables without
a template.
Key Code Snippet 2: Immediate-Sync Alert
sync:true tells the Notecard to try a session right away instead of waiting
for the periodic outbound interval. Notecard for Skylo picks the radio based on
its card.transport wifi-cell-ntn preference, and if WiFi and cellular both
fail, the Note goes over Skylo satellite automatically. This is the single
note.add attempt in sendAlert(). alertType is "fall" or "panic",
eventId is the monotonic counter that beacon_location.qo also carries, and
voltage comes from a card.voltage read just before. If the request fails,
sendAlert() returns false and the caller hands the alert to the retry
queue.
J *req = notecard.newRequest("note.add");
if (!req) return false;
JAddStringToObject(req, "file", "beacon_alert.qo");
JAddBoolToObject(req, "sync", true);
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "type", alertType);
JAddStringToObject(body, "worker_id", g_workerId);
JAddNumberToObject(body, "event_id", (double)eventId);
JAddNumberToObject(body, "voltage", voltage); // -1.0 = read failed
JAddNumberToObject(body, "loc_age_s", locAgeS);
// _lat/_lon populated from Notecard cache by compact template
J *rsp = notecard.requestAndResponse(req);
bool ok = (rsp != NULL && !notecard.responseError(rsp));Key Code Snippet 3: Two-Stage Fall Confirmation
A fall means free-fall (low-g) followed by impact (high-g) within a short
window. Both have to happen, in that order. Either one alone doesn't confirm a
fall. This excerpt from pollFallDetection() runs on every inner-loop sample,
after the bad-read check. A new free-fall phase can't start while an impact
window is still open, and return true hands the confirmed fall to loop().
float totalG = sqrtf(ax*ax + ay*ay + az*az);
uint32_t now = millis();
// ...
// Stage 1: detect and time the free-fall (low-g) phase.
if (!g_inFreefall && !g_watchingImpact) {
if (totalG < g_freefallG) {
g_inFreefall = true;
g_freefallStart = now;
}
} else if (g_inFreefall) {
if (totalG >= g_freefallG) {
if ((now - g_freefallStart) >= g_freefallMinMs) {
g_watchingImpact = true;
g_impactWindowStart = now; // start of window (wraparound-safe)
}
g_inFreefall = false;
}
}
// Stage 2: impact spike within the window.
if (g_watchingImpact) {
if ((now - g_impactWindowStart) >= g_fallWindowMs) {
g_watchingImpact = false; // window expired — not a fall
} else if (totalG > g_impactG) {
g_watchingImpact = false;
g_inFreefall = false;
// ...
return true;
}
}Key Code Snippet 4: Two-Note GPS Flow
The GPS design is non-blocking. The initial alert Note is queued immediately so it can go out without waiting for a fix, and GPS acquisition runs in the background without pausing fall detection or button monitoring.
Step 1: sendAlert(). Before the alert Note is sent, loop() computes
loc_age_s once from a live card.time call and the pre-alert cache epoch
(thisCacheEpoch), which captures the fix age at the moment the event fired.
That value goes straight to sendAlert() and into the retry-queue entry, so
every later retry reports the same original fix age instead of an age relative
to the retry timestamp. The alert Note itself is queued with the Notecard's
cached location embedded via _lat/_lon, sync:true, and the same
event_id on every attempt. beginGpsSearch() only gets called after
sendAlert() returns true, confirming the Note was queued. That ordering
guarantees a failed note.add never leaves an orphaned GPS search. If
sendAlert() fails, enqueueAlert() adds the alert (including its event_id
and pre-computed loc_age_s) to the non-blocking retry queue, and
beginGpsSearch() fires automatically once the retry succeeds inside
pollAlertRetry(). This excerpt is from loop(), with the haptic feedback
calls left out:
float thisLocAgeS = -1.0f;
if (thisCacheEpoch > 0) {
J *tRsp = notecard.requestAndResponse(
notecard.newRequest("card.time"));
if (tRsp && !notecard.responseError(tRsp)) {
uint32_t nowEpoch = (uint32_t)JGetNumber(tRsp, "time");
thisLocAgeS = (nowEpoch >= thisCacheEpoch)
? (float)(nowEpoch - thisCacheEpoch) : -1.0f;
}
if (tRsp) notecard.deleteResponse(tRsp);
}
// ...
uint32_t thisEventId = ++g_alertEventId;
bool sent = sendAlert(alertType, thisEventId, thisLocAgeS);
if (sent) {
g_lastAlertMs = now;
// ...
beginGpsSearch(alertType, thisEventId);
} else {
// sendAlert() makes one attempt. Enqueue for non-blocking
// retry, preserving the original event-time locAgeS and
// event_id so every retry reports the same fix age and the
// same correlation key as this first attempt.
enqueueAlert(alertType, thisCacheEpoch, thisEventId, thisLocAgeS);
// ...
}Step 2: pollGpsSearch(). This runs once per outer loop pass. Polls to
card.location are throttled to once every 2 seconds, since GNSS fixes update
far more slowly than the outer loop runs. It only accepts a fix whose epoch is
later than the pre-alert cache snapshot, so a stale cached fix can't be mistaken
for a new acquisition. On a fresh fix, it queues beacon_location.qo with
sync:true and the same event_id as the initial alert, trying note.add up
to twice before it gives up, and downstream dispatch joins the two Notes using
(device, event_id) as the key. Either way, it then turns GPS off. The part
left out at the top retries a GPS disable that failed earlier.
void pollGpsSearch()
{
// ...
if (!g_gpsSearching) return;
uint32_t now = millis();
// Timeout check (wraparound-safe: compare elapsed, not absolute deadline).
if ((now - g_gpsSearchStart) >= (DEFAULT_GPS_TIMEOUT_SEC * 1000UL)) {
// ...
disableGps();
return;
}
// Throttle card.location polls to once per 2 seconds — GNSS fixes do
// not update at the 10 Hz loop rate; polling faster wastes I2C bandwidth.
if (g_gpsLastPollMs != 0 && (now - g_gpsLastPollMs) < 2000UL) return;
g_gpsLastPollMs = now;
J *rsp = notecard.requestAndResponse(notecard.newRequest("card.location"));
if (!rsp) return;
if (notecard.responseError(rsp)) { notecard.deleteResponse(rsp); return; }
uint32_t fixTime = (uint32_t)JGetNumber(rsp, "time");
double lat = JGetNumber(rsp, "lat");
double lon = JGetNumber(rsp, "lon");
notecard.deleteResponse(rsp);
// Accept only a fix whose epoch post-dates the pre-alert cache snapshot
// to prevent a stale cached fix from being mistaken for a new acquisition.
if (fixTime > g_gpsCacheEpoch && (lat != 0.0 || lon != 0.0)) {
// ...
bool noteOk = false;
for (uint8_t attempt = 0; attempt < 2 && !noteOk; attempt++) {
J *req = notecard.newRequest("note.add");
if (!req) continue;
JAddStringToObject(req, "file", "beacon_location.qo");
JAddBoolToObject(req, "sync", true);
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "type", g_gpsAlertType);
JAddStringToObject(body, "worker_id", g_workerId);
JAddNumberToObject(body, "event_id", (double)g_gpsEventId);
J *locRsp = notecard.requestAndResponse(req);
noteOk = (locRsp != NULL && !notecard.responseError(locRsp));
if (locRsp) notecard.deleteResponse(locRsp);
}
// ...
disableGps();
}
}If GPS times out, only the initial beacon_alert.qo Note is sent, and its
cached location (which may be GNSS-derived, cell-derived, stale, or empty)
stands. The loc_age_s field in that alert records how old the cached fix was
at alert time, so dispatch can judge for itself how fresh the location is.
8. Data Flow
Collected. Every outer loop pass (~10 Hz) reads ten LIS3DH accelerometer
samples at 10 millisecond intervals (100 Hz effective) and runs the two-stage
fall-detection state machine on each one. Each alert event adds battery voltage
(card.voltage), the cached-fix age at alert time (loc_age_s), and a
monotonic event_id. After an alert, the GPS search runs in the background
without blocking, and if it gets a fresh fix, a follow-up Note carries the same
event_id.
Location accuracy. When an alert fires, beacon_alert.qo is queued
immediately with the Notecard's current cached location embedded via
_lat/_lon, plus loc_age_s recording the fix age in seconds (−1 if
unknown). At the same time, a non-blocking GPS search polls card.location
every 2 seconds for up to 90 seconds. If it gets a fix whose epoch is later than
the pre-alert cache snapshot, a beacon_location.qo Note is queued with the
event-time coordinates. If GPS times out, only the initial alert Note is sent,
and the cached location (which may be GNSS-derived, cell-derived, stale, or
empty) stands.
Transmitted.
beacon_alert.qo: sent on a confirmed fall or panic, withsync:trueto trigger immediate cellular (or satellite) transmission. It contains the alert type, worker ID,event_id(monotonic correlation key), battery voltage,loc_age_s(cached-fix age at alert time), and the cached location via the compact template's_lat/_lon.beacon_location.qo: sent after a confirmed alert when a fresh GPS fix arrives within the timeout window, also withsync:true. It contains the alert type (echoing the triggering event), worker ID,event_id(matching the pairedbeacon_alert.qo), and fresh event-time coordinates via_lat/_lon. This Note is optional. It's only queued if GPS succeeds during the background search window.
Routed. Both Notefiles arrive at Notehub with the same event structure
whether they came over cellular, Skylo satellite, or WiFi. Downstream systems
should pair beacon_alert.qo and beacon_location.qo using
(device, event_id) as the join key. event_id is device-local, resets to 0
on each power cycle, and isn't unique across devices on its own. The initial
alert carries the best-available cached location. When GPS acquires within the
90-second background window, a later beacon_location.qo with the same
event_id (from the same device) supersedes it with event-time coordinates.
Pairing on (device, event_id) is unambiguous across repeated alert types,
retries, and network reordering.
Alert triggers:
fall: the two-stage algorithm sees a free-fall phase followed by impact within the detection window. Suppressed if within 60 seconds of the previous alert.panic: the panic button is held forpanic_hold_ms(default 2 seconds). Suppressed if within 60 seconds of the previous alert.
9. Validation and Testing
Expected steady-state behavior. In normal operation, a healthy beacon
produces zero beacon_alert.qo events and zero beacon_location.qo events. To
check that Notecard provisioning, template registration, and connectivity all
work after assembly, trigger a test fall or panic (see below). The event should
show up in Notehub once a session is established, typically within 30–90
seconds on cellular. If it doesn't, check the Notecard's sync status in the
Blues In-Browser Terminal ({"req":"hub.sync.status"}) to see
the last sync time, whether Notes are still waiting to sync, and the last sync
error.
Simulating a fall. For a realistic fall simulation on the bench, hold the
device 50–80 cm above a padded surface and drop it. The
firmware's default thresholds (0.55g free-fall, 2.5g impact) are tuned for
human-body-scale falls. You should feel the haptic motor pulse twice
(non-blocking, so monitoring continues during the buzz sequence), and a
beacon_alert.qo Note with "type":"fall" should appear in Notehub once a
session is established, typically within 30–90 seconds on cellular. If the
device gets a fresh GPS fix during the background search window, a follow-up
beacon_location.qo Note with the event-time coordinates will show up shortly
after. In poor GNSS conditions (indoors, obstructed sky view), the background
search times out after 90 seconds and only the initial alert Note is sent.
Simulating a panic. Hold the button for 2+ seconds. Once the 30 millisecond
debounce settles on the press edge, the haptic motor gives one click to confirm
the press registered. When the hold reaches the threshold, the firmware checks
the 60-second alert cooldown before queuing anything. If the cooldown has
expired, the panic alert is accepted and three haptic buzzes confirm it's
queued for transmission. They're non-blocking, firing 220 milliseconds apart
without pausing button monitoring. The triple buzz means the alert has been
accepted for transmission handling. It went either straight into the Notecard's
outbound queue (if note.add succeeded) or into the firmware's local retry
queue, for delivery as soon as the Notecard is reachable (if note.add failed
transiently). If the device is still inside the cooldown window, a single buzz
acknowledges the hold without queuing an alert, so use Notehub's event log to
tell a suppressed panic from a queued one. A beacon_alert.qo with
"type":"panic" should reach Notehub once a session is established. If GPS gets
a fresh fix during the background search, a beacon_location.qo follow-up Note
will show up as well.
Simulating satellite failover. With the device outdoors (so the MAIN
antenna can see the sky), force a satellite session by temporarily restricting
Notecard for Skylo to NTN-only. Send {"req":"card.transport","method":"ntn"}
from the blues.dev In-Browser Terminal, then trigger a panic. The alert should
arrive in Notehub over the satellite path, and the event metadata will show
"transport":"ntn" to confirm it. Afterward, reset the transport to restore
automatic failover: {"req":"card.transport","method":"wifi-cell-ntn"}.
Power validation with Mojo. The Mojo sits
inline between the LiPo and the Notecarrier CX +VBAT pad. It adds up the
charge the board stack consumes and makes the reading available over its Qwiic
I²C link. The beacon firmware does not read the Mojo, because the Mojo is
only a bench measurement instrument here. To get the mAh reading during
validation, connect a separate Qwiic-capable host running the Mojo readout
sketch to the Mojo's Qwiic connector. A SparkFun RedBoard Qwiic, a Notecarrier
AL with its own sketch, or any I²C host with a Qwiic port will do. Run that host
alongside the beacon during your validation window, and its USB serial output
shows the mAh total climbing in real time without touching the beacon firmware.
Battery runtime estimate (1200 mAh LiPo, based on measured and published figures):
Assuming steady-state idle (no alerts) and a daily cellular sync:
- Steady-state draw: ~10–20 mA (host + Notecard idle)
- Daily sync duration: ~2 minutes (session setup + Note transmission)
- Daily sync energy: ~8 mAh (~250 mA × 2 minutes)
- Idle energy per 24h: ~240–480 mAh (steady-state) + ~8 mAh (sync) = ~250–490 mAh/day
- 1200 mAh battery: ~2.5–5 days on steady-state + 1 sync/day, with zero alerts
Alert overhead (per event):
- GPS acquisition (if outdoors, 90 seconds typical): +30–50 mA × 1.5 minutes = ~0.75–1.25 mAh per alert
- Cellular sync for alert Note: ~8 mAh (another ~2-minute session)
- Total per alert: ~9–10 mAh
- At one alert per day: ~2.4–4.7 days total
Validation with Mojo is mandatory before deployment. Measure your actual steady-state draw and per-alert overhead. WiFi and satellite transports have different energy profiles, and sync cadence and GPS timeout settings both affect total runtime.
Published Notecard for Skylo figures (from Blues documentation)
| Component | State | Published figure |
|---|---|---|
| Notecard for Skylo (NOTE-NBGLWX) | Idle, radio off (between sessions) | ~8 µA (datasheet) |
| Notecard for Skylo (NOTE-NBGLWX) | Network session (cellular or satellite) | ~250 mA average from the onboard BG95-S5 modem; brief peaks up to ~2 A for a few ms on a 2G transmit burst (datasheet) |
Estimated whole-device figures (validate with Mojo before sizing for deployment)
| State | Estimated draw | Notes |
|---|---|---|
| Host awake, Notecard idle (steady-state background) | ~10–20 mA | STM32L433 at default clock, not from a Blues datasheet. Disable debug Serial to reduce clock load. |
| GNSS acquisition active (up to 90 seconds per alert event) | +30–50 mA above baseline | Notecard GNSS module draw; validate with Mojo |
| Haptic motor active (ERM disc, ~0.5 seconds per buzz) | +50–80 mA above baseline | DRV2605L + motor; validate with Mojo |
| Alert event (GPS acquisition + cellular sync) | ~40–70 mA for up to 90 seconds | Dominant transient; rare in normal operation; validate with Mojo |
Here's a productive bench exercise. Run the device for 2 hours on a known-capacity LiPo and note the Mojo's mAh reading. Trigger several test falls and panics, and compare the per-sync energy spikes against the alert count. Between alerts, the steady-state draw should stay close to the host-idle estimate (~10–20 mA). If it's well above 20 mA, the host MCU is probably running at full clock. Try disabling debug serial output, which forces the STM32 to keep its USB clock running. To project battery runtime for your specific alert frequency and transport conditions, use the Mojo-measured average current, not the steady-state estimate.
10. Troubleshooting
Device does not appear in Notehub.
- Check that the
PRODUCT_UIDinlone_worker_beacon_helpers.hmatches your Notehub project's ProductUID exactly. - Check that the Notecard has a provisioned SIM. It should ship with one, which you can confirm at the Blues shop.
- Make sure cellular or WiFi coverage is available. If you're indoors with no WiFi provisioned, power cycle and move to a window or open area.
- From the Blues In-Browser Terminal, send
{"req":"hub.sync.status"}. A smallcompletedvalue (seconds since the last sync) means the Notecard is communicating. Ifcompletedis missing or large, oralertistrue, the Notecard can't reach cellular or Notehub.
Panic button or fall detection does not trigger.
- Check the haptic motor for signs of life. Power on the beacon, wait 5 seconds for boot to finish, then hold the panic button for 3+ seconds. You should feel a vibration within 1–2 seconds of pressing. If you don't, check the motor's wiring to the DRV2605L and make sure the DRV2605L is seated on I²C (address 0x5A).
- Make sure the LIS3DH is responding. If you see no double-buzz fault pattern after boot, the accelerometer initialized. Try triggering a fall by dropping the beacon 50–80 cm onto a pillow or padded surface. If there's still no double-buzz for the alert, check the LIS3DH's I²C wiring (SDA, SCL, 0x18 address) and that the SDO pin is grounded or left floating.
- Check for the startup fault pattern. A slow double-buzz (buzz, pause, buzz)
repeating every 5 seconds means a boot-time configuration error (missing
PRODUCT_UID, failedhub.set, or template registration failure). Verify thePRODUCT_UIDand power-cycle the beacon.
Alerts appear in Notehub but coordinates are missing or stale.
loc_age_s: -1.0means the Notecard had no cached GPS fix when the alert fired, which is normal indoors. If you're outdoors with a clear view of the sky andloc_age_sis still -1 after several alerts, the Notecard may not have acquired a GPS fix since power-on. Run{"req":"card.location"}from the Blues In-Browser Terminal to see what the Notecard has cached. That request doesn't start a new fix. The firmware only turns GPS on for the 90-second search after an alert, so trigger a test alert outdoors and checkcard.locationagain.- If there's no
beacon_location.qofollow-up Note, GPS didn't get a fresh fix within the 90-second background search window. That's normal indoors or under heavy tree cover. If GPS should be working, trigger a test alert and check the Notecard'scard.locationresponse for lock time and signal quality.
Battery drains too quickly.
- Make sure the host MCU isn't stuck in a high-clock state. Debug output is off
by default (
#define DEBUG_SERIALis commented out inlone_worker_beacon_helpers.h). If you turned it on for bench work, the debug serial forces the STM32 to keep its USB clock running, so comment it out again to cut MCU idle from ~10–15 mA to ~5–10 mA. - Confirm the Notecard is in low-power periodic mode by sending
{"req":"hub.get"}from the Blues In-Browser Terminal. Ifoutboundis less than 1440 (24 hours) orinboundis less than 120 (2 hours), the sync cadence may be excessive. Low-Power Strategy has the settings the firmware uses. - Use Mojo (see Section 9) to measure the actual current in each state (idle, fall/panic event, GPS search, sync), and compare it against the estimated figures in the power validation table. If measured idle is >25 mA, a peripheral may be drawing more than expected. Check I²C for clock-stretching issues or peripheral misconfigurations.
Skylo satellite transmission fails or never completes.
- The Skylo NTN path is only active after Notecard for Skylo has completed at least one successful cellular or WiFi sync to associate with your project and deliver the compact template definitions to Notehub. If the beacon starts up in a no-cellular area, it can't use satellite until it has found cellular (or WiFi) coverage at least once. Move the device into coverage, power it on, wait for a sync (check the device status in Notehub), then move back to the satellite-only zone.
- Check the
MAINantenna orientation. It should face the sky (the polycarbonate enclosure top is enough), and in the northern hemisphere a southward orientation improves link margin. If the antenna is pressed against a body or pointed away from the sky, Skylo acquisition may fail. - Check Skylo coverage. The Skylo network covers specific regions (see the Notecard for Skylo datasheet), and outside the coverage footprint, NTN transmission will fail silently.
- From the Blues In-Browser Terminal, send
{"req":"card.transport","method":"ntn"}to force satellite-only operation (cellular and WiFi disabled). Trigger a test panic from a spot with a view of the sky, then check the event metadata. If"transport":"ntn"is there, the satellite path is working. Re-enable automatic failover afterward with{"req":"card.transport","method":"wifi-cell-ntn"}.
Fall detection generates false positives during normal work.
- Adjust the
freefall_gandimpact_gthresholds with environment variables (see Section 6, step 5). Lowerfreefall_g(e.g., 0.45 or 0.35) to require a deeper free-fall phase before the impact window opens. Increaseimpact_g(e.g., 3.0 or 3.5) to require a larger acceleration spike to confirm impact. Both changes reduce sensitivity and may suppress legitimate falls, so validate them against your specific worker activity profile. - Shorten
fall_window_ms(e.g., 300 milliseconds instead of 500 milliseconds) to close the impact window sooner, so the impact has to follow the free-fall phase more closely. That rejects impact spikes that arrive more than 300 milliseconds after the free-fall phase ends. - New values can take up to 4 hours to reach the device (see Section 6, step 5), so give a change time to land before you judge it.
- Run a 24-hour learning period with each worker activity profile and log the
LIS3DH's total-g readings. The firmware doesn't record them, so add a debug
print or a telemetry Note for the test. Find the baseline g-profile of normal
work (walking, climbing, tool swings). Then set
impact_gjust above the highest peak andfreefall_gjust below the lowest dip you see during normal use.
Multiple alerts fire from a single fall.
- The 60-second alert cooldown (
DEFAULT_ALERT_COOLDOWN_SEC) prevents alert storms, and that's intentional. A second fall detected within 60 seconds of the previous alert is suppressed with no haptic feedback. (A suppressed panic gets one buzz instead of three.) If you need multiple alerts for a tumbling device, shorten the cooldown in the firmware or set it through an environment variable (once a future revision supports that).
11. Limitations and Next Steps
This is a reference design, not a finished safety product. A production team still has to add several things a real lone-worker fleet would demand, most notably a certified safety claim and a sleeping host MCU. They're listed below as deliberate scope choices, so none of them should come as a surprise. After the limitations comes the work that turns this into a deployable wearable.
Simplified for the POC
Each item here is something this POC leaves out on purpose and a real lone-worker fleet would add before deployment.
Not a certified life-safety device. This proof-of-concept has not been evaluated for fail-safe operation or certified under any personal-emergency-response or lone-worker protection standard. Validate every alert behavior against your own safety requirements, and treat this design as a starting point rather than a production safety system. Use it only to supplement mandatory check-in procedures, required PPE, and any regulated safety systems that apply to your operation. Do not use it to replace any of them.
Host MCU never sleeps. The firmware runs a dual-cadence polling loop. The LIS3DH is sampled at ~100 Hz through a 10-sample inner loop (10 milliseconds between reads), and Notecard/GPS/haptic state advances at the outer ~10 Hz cadence. A production implementation should use STM32L433 STOP2 mode with GPIO interrupt wakeup from the LIS3DH INT1 pin and the panic button, which drops host idle draw from ~10+ mA to ~2–3 µA. The LIS3DH's hardware free-fall interrupt handles Stage 1 detection in silicon with no host MCU involvement, so the host only wakes when the interrupt fires. This is the single biggest power optimization available and the most important production change.
Fall detection is software-sampled. The LIS3DH is sampled at ~100 Hz by the
inner loop (10 reads per outer pass, 10 milliseconds apart), which matches the
sensor ODR and reliably catches free-fall phases as short as 80 ms. The
remaining limitation is that Stage 1 (free-fall detection) runs in host firmware
instead of the LIS3DH hardware interrupt registers, so the host MCU has to stay
awake continuously. The production upgrade is the LIS3DH INT1 free-fall
interrupt (through the INT1_CFG, INT1_THS, and INT1_DURATION registers).
It offloads Stage 1 entirely to the accelerometer silicon, lets the STM32L433
sleep in STOP2 between events, and eliminates the polling overhead.
Fall detection thresholds are heuristic. The default 0.55g free-fall / 2.5g impact thresholds cover textbook falls from standing height onto hard surfaces. They will produce false negatives for soft-surface landings (carpeted floors, mud) where the impact spike is attenuated, and they may produce false positives during vigorous physical work involving overhead tool swings. Production deployments should run a calibration period with each worker activity profile before enabling real-time dispatch.
No cancel flow after panic. The current firmware gives a worker no way to
cancel a panic alert once it's been confirmed and sent. A production device
should include a multi-step cancel: a button press within 60 seconds of a
panic, haptic confirmation, and a cancel Note the dispatch system can act on.
GPS follow-up Note is optional and serialized. When a fall or panic alert
fires, the initial beacon_alert.qo Note is queued immediately with the
Notecard's cached location. The background GPS search then runs for up to
DEFAULT_GPS_TIMEOUT_SEC (90 seconds) without blocking the detection loop. If
a fresh fix arrives, a beacon_location.qo Note is queued with the event-time
coordinates and the same event_id. GPS can time out because the device is
indoors, under heavy tree cover, or the Notecard has no sky view. Then only the
initial alert Note is delivered, and its cached location (which may be stale or
empty) stands. Dispatch should treat a missing beacon_location.qo (matching
event_id) as a sign that the event-time position is unknown, not that the
alert failed. Only one GPS enrichment window can run at a time. A second alert
that fires during the 90-second window (the 60-second cooldown is shorter than
the GPS timeout) gets only its cached location and no beacon_location.qo
follow-up. A production system that requires fresh GPS for every alert should
serialize the alert cadence (e.g., extend the cooldown to match the GPS
timeout) or implement a per-alert GPS job queue.
No data encryption. beacon_alert.qo isn't a secure Notefile, so cellular
sessions aren't forced onto TLS, and the Notes are readable in Notehub. Naming
it beacon_alert.qos adds transport encryption to Notehub (TLS on the cellular
path, ChaCha20-Poly1305 packet encryption on the satellite path), but Notehub
still sees the contents. Payload
encryption
only covers the Note body, and this beacon's coordinates aren't in the body.
The _lat/_lon template fields have Notecard attach its cached location,
which Notehub reports as the event location (best_lat/best_lon), and
Notehub can append its own cell tower and WiFi location estimates to events
too.
Skylo NTN requires an initial non-NTN sync. The Skylo satellite (NTN) path isn't available until Notecard for Skylo has completed at least one successful cellular or WiFi session to associate with Notehub and register the Notefile templates. A device that ships straight into a no-cellular zone can't send via satellite until it has found cellular (or WiFi) coverage at least once. The standard mitigation is to pre-provision each device during QA on a cellular-capable bench.
Satellite payload budget. Notecard for Skylo's bundle includes 10 KB of
Skylo satellite data, and the Skylo NTN link enforces a hard 256-byte maximum
per Note. The compact alert template is well under that ceiling, but frequent
triggering in a no-cellular environment will use up the bundle faster. The
2-hour inbound check runs over satellite too, and each one uses about 50
bytes, so a beacon that stays on Skylo spends roughly 600 bytes a day before
it sends a single alert. Setting the _sync_inbound_mins_ntn reserved
environment
variable
while the beacon still has cellular coverage stretches that interval on
satellite only, with no firmware change, at the cost of slower threshold and
worker_id updates while the beacon is on Skylo.
Single I²C bus for all peripherals. The LIS3DH and DRV2605L share the bus with the internal Notecard connection. A severe I²C lockup (e.g., a partially-completed transaction interrupted by a reset) could block all communication, so a production design should include bus-error recovery and a hardware watchdog.
No charging subsystem. This POC documents a bare LiPo cell, and the BOM and wiring include no charger, dock, cradle, or inductive charging coil. A production wearable needs an appropriate charging path, for example a USB-C LiPo charging circuit built into the enclosure, plus overcharge and short-circuit protection if the cell's built-in circuitry doesn't already provide it. Rechargeable operation is a natural next step but is out of scope for the POC.
Mojo is bench-only in this POC. The firmware doesn't read the Mojo's charge
accumulation register over Qwiic, because the Mojo is only a bench measurement
instrument here. A production extension could add a mah_consumed field to
beacon_alert.qo for fleet-level battery-health monitoring, read over the
Mojo's Qwiic I²C link.
Production Next Steps
To turn this into a deployable wearable, start with the power and safety changes that matter most and work down to per-worker refinements.
STM32L433 STOP2 low-power mode with LIS3DH INT1 hardware interrupt wakeup is the single most impactful power optimization. It drops host idle from ~10–15 mA to ~2–3 µA.
LIS3DH hardware free-fall and shock detection configured through the
INT1_CFG, INT1_THS, and INT1_DURATION registers offloads Stage 1
detection entirely to the accelerometer silicon.
A cancel-alert flow gives the worker a way out of a false alarm. The worker
would confirm a cancel within N seconds of the panic, and it would go out as a
cancel event on beacon_alert.qo carrying the same event_id as the alert
being canceled.
Worker check-in acknowledgment closes the loop with the worker. A dispatcher can send a Notehub Signal back to the device to trigger a distinctive haptic pattern, so the worker knows their alert was received.
Field-upgradeable firmware through Notecard Outboard DFU lets you push threshold recipes to the whole fleet without a physical re-flash.
A .qos alert Notefile adds transport encryption between Notecard and
Notehub on both the cellular and satellite paths. Keeping worker location
private from Notehub too, for privacy-sensitive jurisdictions, is a bigger
change. The coordinates would have to move out of the _lat/_lon template
fields into body fields protected by payload
encryption,
and Notehub would still append its own cell tower and WiFi location estimates to
events.
Per-worker baseline calibration records each worker's typical activity
vibration profile over a 24-hour learning period, then tunes impact_g and
freefall_g for that worker.
12. Summary
The lineman at the edge of the substation, the pumper at the rural wellhead, and the field tech in the 2 AM boiler room now each clip on a device that does what no check-in procedure ever could. It watches them automatically, with no action required from the worker, and it reaches a dispatcher even when cellular goes dark. The two-stage fall algorithm filters out most everyday bumps, though soft landings can still slip past it (see Limitations), and the panic button covers the situations that don't look like physics. Notecard for Skylo's onboard satellite radio covers the specific sites where cellular fails first and matters most, with no companion module or second device. The cellular path handles the vast majority of activations quickly and cheaply, and the satellite path is the safety margin underneath it. Put that combination in a belt-clip enclosure and you have a practical form of lone-worker safety assurance. It supplements the procedures and PPE that came before it and does not replace them.