Heavy Equipment Hours-of-Use & Utilization Tracker
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/equipment_hours_tracker/ so it can learn how the device works and help you adapt it to your hardware.
This project is a retrofit asset location tracking solution for excavators, generators, compactors, light towers, and any other mobile heavy equipment that a rental company or OEM needs to bill by the hour and service on schedule. A magnetically mounted, solar-trickle-charged enclosure uses a 3-axis accelerometer to pick up engine on/off transitions from their vibration signature. It keeps a software hour meter and reports location and utilization to the Blues Notehub cloud service over cellular or satellite, without a wiring harness, changes to the equipment, or a job-site network. Skylo-supported satellite fallback keeps the device reporting from remote pipeline corridors, open-pit mines, and wind-farm construction zones where terrestrial coverage runs thin. The hardware is a Notecarrier CX with a Notecard for Skylo and an external IMU (§4 has the BOM).
1. Project Overview
The problem. Rental heavy equipment earns its money by the hour. The excavator a contractor rented on Monday morning needs an accurate engine-hour count when it's billed on Friday afternoon. It also needs a warranty-hours check before its next delivery and a predictive-maintenance flag at each 250-hour service interval. Hardwired telematics (OBD-II interfaces, CAN-bus taps, hour-meter relays) work well on newly acquired machines. On older ones they're expensive to retrofit, installing them often takes the equipment out of service, and they can void the warranty when they involve getting into the engine control unit.
Vibration-based hour detection sidesteps the retrofit problem entirely. A small enclosure stuck magnetically to the equipment frame asks one question on every sample: is this machine's engine running right now? That's harder to answer than it sounds. An excavator riding to a job site on the back of a flatbed has its engine off, but to a simple threshold-based accelerometer, the truck's diesel vibration and road shock look a lot like engine idle. This project handles that with a two-parameter vibration signature. The root-mean-square (RMS) amplitude of the net acceleration residual measures how much activity there is, and the coefficient of variation (CV, or σ/μ) separates steady, periodic engine vibration from the irregular, bursty shock of transport. Engine idle gives a low CV and road vibration gives a high one. Together they sort the machine into three reliable states (engine running, in transport, and idle/stopped) without a single wire to the equipment.
Why Notecard for Skylo. This equipment moves from one customer job site to the next. Those sites have no WiFi, and cellular coverage there is often marginal or missing. Open-pit mines, remote pipeline corridors, and offshore wind-farm construction zones are typical. Notecard for Skylo (NOTE-NBGLWX) puts LTE-M, NB-IoT, GPRS, Skylo satellite NTN (non-terrestrial network), and WiFi fallback on a single M.2 module, so you don't need a separate satellite modem or a Starnote companion board. When cellular is available, Notes go out over LTE-M with the low latency and high throughput you'd expect. When the equipment sits at the bottom of a quarry or behind a ridge no tower can reach, the Notecard falls back on its own to a satellite uplink through Skylo's NTN service. The operator gets unbroken location and hours telemetry no matter what the site looks like, and the firmware never needs to know which network carried it.
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. The enclosure sticks magnetically to any ferrous chassis surface, such as a frame rail, a tool-box lid, or a battery tray. You don't drill holes, run a wiring harness, or need anything from the OEM. A small solar panel epoxied or bolted to the top of the enclosure trickle-charges a 2000 mAh LiPo through the Notecarrier CX's built-in solar charging circuit. The equipment can sit unused on a lot for weeks while the tracker holds its charge. When the operator fires up the machine on a remote site, the vibration classifier catches the engine start and the Notecard sends the telemetry over whatever network it can reach.
2. System Architecture
Device-side responsibilities. Three jobs happen on the equipment itself,
and all of them have to fit in a 30-second wake budget. The Notecarrier CX's
STM32 host wakes when the Notecard raises its ATTN pin at the end of a
card.attn sleep. It
reads the Adafruit LSM6DSOX accelerometer over I2C and grabs a 2-second burst of
3-axis samples at 104 Hz. The vibration classifier turns that burst into one of
three states (IDLE, RUNNING, or TRANSPORT), feeds the hour-meter accumulator,
and compares the result with the previous wake. A state change fires an event
right away, and an elapsed summary window fires a summary Note. Between wakes
the host sleeps in STM32 STOP2 (a few µA) with its RAM intact. The hour meter
and event queue just stay in memory until the Notecard's ATTN pin wakes the
host for the next sample.
Notecard responsibilities. Once the host hands off, Notecard for Skylo
handles the network side. It queues Notes
locally and opens cellular or satellite sessions on two cadences set in
hub.set. A daily outbound
sync carries the queued summaries, and an 8-hour inbound check-in
(inbound: 480) pulls environment-variable updates from Notehub. After each
state-change event lands in the queue, the firmware sends a separate hub.sync
so the billing record doesn't sit waiting for the next scheduled outbound
window. Periodic GPS location sampling (every 15 minutes) and ringfence setup
both go through
card.location.mode,
and a separate
card.location.track
call turns on the 4-hour _track.qo heartbeat record. If you configure a
ringfence through environment
variables,
the Notecard checks it on its own and emits a _track.qo event the moment the
equipment leaves the job site. After the ringfence parameters are applied, the
host doesn't have to do anything.
Notehub responsibilities. The Notecard's embedded global SIM handles cellular and Skylo NTN satellite sessions on supported carriers worldwide and delivers events to Notehub over the Internet. From there, Notehub ingests and stores them and applies your project-level routes. Operators never touch firmware to retune the fleet. Fleet-level environment variables let you adjust vibration thresholds and ringfence parameters from the web console without a truck roll, and Smart Fleets group devices by rental customer, equipment class, or geographic territory so routing and alerting can differ for each group.
Routing to the cloud (high level only). Notehub can route to HTTP, MQTT, AWS
IoT Core, Azure IoT Hub, GCP Pub/Sub, Snowflake, and other destinations.
State-change events (equip_event.qo) make sense to send to an on-call webhook
or work-order system, and daily summaries (equip_summary.qo) fit a
time-series historian or billing database. This project doesn't ship a specific
downstream endpoint, so see the Notehub routing
docs to set one up.
3. Technical Summary
- Create a Notehub project and copy your ProductUID.
- Wire up the bench: Notecarrier CX + NOTE-NBGLWX + LSM6DSOX on I2C, plus the
ATTN→D5jumper for host sleep (§5 has the pinout). - In
firmware/equipment_hours_tracker/equipment_hours_tracker_helpers.h, replace the empty#define PRODUCT_UID ""with your project's value (format:com.your-company.your-name:tracker). - Install the board core:
arduino-cli core install STMicroelectronics:stm32 --additional-urls https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json - Compile and upload (the full
arduino-clicommands with your port are in §7.1). - Open a serial monitor at 115200 baud on the ST-LINK virtual COM port from
the Notecarrier CX debug jack (see §7.1). You
should see
[BOOT] Notecard configured, followed by[VIB]log lines every 30 seconds. - Power the device, then open Notehub → your project → Events tab.
_session.qoshould show up within ~1 minute on cellular (longer on satellite, depending on sky view). Tap the enclosure to make some vibration, and state-change events will appear inequip_event.qo.
Here's a sample Note from this device:
{
"file": "equip_summary.qo",
"body": {
"run_h": 6.25,
"run_h_total": 1253.75,
"transport_h": 1.08,
"bat_v": 4.07,
"fault_ct": 0
}
}4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an onboard STM32L433 host. Solar/LiPo charging circuits, ATTN on the host header for sleep/wake, and I2C break-outs all fit on one compact board, so you don't need a separate MCU. The host sleeps in STOP2 between samples until the Notecard's ATTN pin wakes it. |
| Notecard for Skylo (NOTE-NBGLWX) | 1 | Single M.2 module that combines LTE-M/NB-IoT/GPRS, Skylo satellite NTN, WiFi, and GPS/GNSS. It's cellular-first and falls back to satellite automatically. Check the datasheet for regional coverage. The device comes with 500 MB of cellular data and 10 KB of satellite data. |
| Blues Mojo | 1 | Coulomb counter that sits inline on the load rail (LiPo → VBAT) to measure real consumption during bench bring-up. It confirms the host sleep/wake pattern and the whole-device energy draw. It only measures the load side, not the solar charger input, and you don't need it in production. |
| Adafruit LSM6DSOX 6-DoF IMU breakout (#4517) | 1 | ST LSM6DSOX 3-axis accelerometer + gyroscope on I2C. The firmware only uses the accelerometer axes and explicitly shuts the gyroscope down. The STEMMA QT/Qwiic connector turns the bench wiring into a single cable. |
| Adafruit 6V 1W solar panel (#3809) | 1 | The 6V output falls within the Notecarrier CX solar input range (4.5–7V). 1W is enough to trickle-charge a 2000 mAh LiPo with ≥2–3 hours of sun a day. |
| 3.7V LiPo, 2000 mAh, JST-PH 2-pin (e.g. Adafruit #2011) | 1 | Single-cell LiPo for overnight and cloudy-day runtime. §9 estimates whole-device consumption at 65–120 mAh/day, which accounts for three daily inbound check-in sessions plus one outbound sync. At that rate, with an 80% practical depth of discharge, 2000 mAh gives you roughly 13–24 days of operation with no solar input at the default 30-second sample cadence. Real runtime depends on network type (satellite draws more than LTE-M), signal quality, and how often GNSS fixes succeed, so validate it with Mojo at your own site. |
| K&J Magnetics MM-C-36 neodymium pot magnet + MM-RC-36 rubber cover | 1 | 36 mm face, M6 × 1.0 external threaded stud, and 41 kg (90 lb) of pull force, far more than the ~250–350 g assembled enclosure weighs. The nickel-coated steel cup concentrates the flux, and the MM-RC-36 silicone over-cap (~1 mm thick) protects the chassis finish and cuts down on sliding. The max continuous operating temperature is 80 °C. That's fine for frame rails and battery trays, but keep it off exhaust and turbocharger surfaces. Regional alternative: any rubber-coated 36–42 mm M6-stud neodymium pot magnet rated ≥15 kg from RS Components or Grainger is a direct substitute. |
| IP67 weatherproof enclosure, ~130 × 80 × 55 mm (e.g. Hammond 1554B2GY) | 1 | Polycarbonate / ABS housing that protects the electronics from rain, jet wash, and splashing. GPS signals pass through the plastic lid, so the GPS antenna can go inside. It's sized to fit the Notecarrier CX, the LiPo, and cable management. |
| Passive GPS/GNSS patch antenna, u.FL connector (e.g. Adafruit #2460) | 1 | Attaches to the NOTE-NBGLWX GPS u.FL port. Must be passive, because the GPS port doesn't supply DC bias and the datasheet explicitly requires a passive antenna. The 50 mm pigtail is long enough to put the 9 mm × 9 mm ceramic patch against the inside face of the polycarbonate lid, and GPS L1 signals get through the plastic without a cable gland. Point the patch at the sky for the best fix rate. |
| u.FL-to-SMA female pigtail cable, ~150 mm (e.g. Adafruit #851) | 1 | Brings the NOTE-NBGLWX MAIN u.FL port through the enclosure wall to the Skylo-certified MAIN antenna. The u.FL end (female, clips onto the Notecard's MAIN port) stays inside the enclosure. The other end is an SMA female jack (panel-mount style). Pass it through a drilled or punched hole in the enclosure sidewall, secure it with the hex nut, and run a thin bead of silicone sealant under the nut face so the enclosure keeps its weatherproof rating at the penetration. The Skylo-certified MAIN antenna's SMA male plug screws straight onto this SMA female from outside the enclosure, so you don't need a separate bulkhead adapter. The MAIN port carries both LTE-M/NB-IoT/GPRS cellular and Skylo NTN satellite on a single antenna path (S-Band / L-Band, bands B23/B255/B256). The Skylo-certified MAIN antenna included with the NOTE-NBGLWX must be used on this port. A different antenna loses the Skylo certification, and Skylo may block the device from its network. Mount the MAIN antenna outside the enclosure with an unobstructed view of the sky. The NOTE-NBGLWX datasheet has the full antenna requirements. |
Notecard for Skylo ships with a factory-provisioned SIM that includes global cellular and satellite service, 500 MB of cellular data, 10 KB of satellite data, and 10 years of connectivity in the device price, with no activation fees and no monthly minimum. That service comes with Notecard for Skylo only. The Notecarrier CX and Mojo are hardware, with no connectivity service attached.
5. Wiring and Assembly
Notecard for Skylo seats in the Notecarrier CX's M.2 Key E slot. The Adafruit LSM6DSOX connects to the Notecarrier CX's I2C header. If you have the matching Qwiic/STEMMA QT cable, that's a single snap at each end. Otherwise, use four separate wires. The Mojo sits inline on the main power feed during bench validation.
Pin-by-pin:
- +3V3 → LSM6DSOX
VCC(3.3 V logic. The sensor is 3.3V only, so don't use 5V.) - GND → LSM6DSOX
GND - SDA → LSM6DSOX
SDA(the Notecarrier CX has 4.7 kΩ pull-ups on-board) - SCL → LSM6DSOX
SCL - Solar JST → 6V solar panel (match the polarity marked on the connector, where red = positive)
- LiPo JST → 3.7V 2000 mAh LiPo cell (JST-PH 2-pin, same polarity convention)
- ATTN → D5 jumper (host sleep). Fit a short jumper between the ATTN and
D5 pins on the Notecarrier CX. Both are on the same 16-pin header (
ATTNat position 3 andD5at position 14, per the Notecarrier CX datasheet header description). The Notecard pullsATTNlow while the host sleeps and raises it when the next sample is due. The rising edge onD5wakes the host from STOP2, andloop()picks up where it left off. 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. Without theATTN→D5jumper, the firmware sees thatATTNnever went low and waits out each 30-second interval awake. Sampling still works, but baseline current lands at ~5–10 mA instead of the ~20–60 µA target, and the solar/battery budget in §9 won't hold. Putting a Host to Sleep Between Sensor Readings walks through the full pattern, and thecard.attnAPI reference covers the request itself.
Antenna placement. The NOTE-NBGLWX has exactly two u.FL antenna ports:
MAINu.FL: carries both LTE-M/NB-IoT/GPRS cellular and Skylo NTN satellite on a single path. Clip the u.FL end of the pigtail (Adafruit #851 or an equivalent u.FL-to-SMA-female-jack cable) onto theMAINu.FL port, then route the cable to a drilled or punched hole in the enclosure sidewall. Push the pigtail's SMA female jack (the panel-mount end) through the hole from inside, thread the hex nut on from outside to clamp it against the wall, and run a thin bead of silicone sealant under the nut face. That bead is the weatherproof seal at the RF penetration. From outside the enclosure, screw the SMA male plug of the Skylo-certified MAIN antenna (included with the NOTE-NBGLWX) onto the exposed SMA female, and point the antenna up with a clear, unobstructed view of the sky. Skylo NTN needs the antenna outdoors, so don't mount it under steel overhead panels or inside equipment compartments.GPSu.FL: passive GNSS only, so don't connect an active (bias-powered) antenna. Attach the Adafruit #2460 passive GPS patch here and set it against the inside face of the polycarbonate lid. GPS L1 signals get through the plastic without a cable gland.
The MAIN antenna is Skylo-certified as a unit with the NOTE-NBGLWX. Don't swap in a different antenna, and don't add any extension cable beyond the short u.FL-to-SMA-female pigtail that brings the MAIN port through the enclosure wall.
Mojo placement (bench only). During bench measurements, Mojo replaces
the normal battery feed for the whole test. It doesn't splice in alongside the
LiPo. Before you insert Mojo, disconnect the LiPo from the Notecarrier CX's
LiPo JST. With that JST unplugged, route power through the Mojo: bench supply
(or the LiPo brought out on a bare lead) → Mojo BAT input → Mojo LOAD output
→ Notecarrier CX VBAT pad. If you leave the battery on the JST while also
feeding VBAT through the Mojo, you create a parallel power path that skips the
intended measurement point and risks backfeeding through the charger circuit.
Disconnect the solar panel too. The solar input is a separate rail, and if it's
left connected it will offset the load draw.
Accelerometer orientation. You can mount the LSM6DSOX any way up. The firmware computes the combined 3-axis magnitude and subtracts the 1g gravity baseline, so the axes don't need to line up with anything. What does matter is rigidity. Fix the breakout board solidly to the inside of the enclosure (hot glue or standoffs) so it vibrates with the equipment chassis instead of floating on its wires.
Mounting safety. Heavy equipment works in rough conditions. Treat the magnetic mount as the primary attachment, and add extra protection before it goes into the field:
- Secondary retention. Add a safety tether, a short loop of steel wire or nylon strap tied between an M6 bolt on the enclosure and a nearby chassis fixture. If the magnet lets go of a painted or lightly corroded surface, the tether catches the enclosure before it drops into moving linkages or the undercarriage.
- Surface temperature. Neodymium magnets start losing pull force above ~80 °C and can demagnetize permanently above ~150 °C. Don't mount on exhaust manifolds, muffler housings, turbocharger bodies, or any chassis surface that radiates heat from the engine bay. Frame rails, battery trays, and tool-box lids usually stay within a safe temperature range.
- Moving parts and pinch zones. Stay clear of pivot pins, hydraulic cylinder clevises, boom linkages, and undercarriage rails, where the enclosure could get crushed or snagged as the machine articulates.
- Washdown exposure. The enclosure must be IP67-rated, with every cable gland properly seated and the lid gasket undamaged. A pressure washer (common on construction sites) can exceed IP67 limits if it's aimed straight at a gland, so point the glands away from the main washdown direction.
6. Notehub Setup
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID (format:
com.your-company.your-name:equipment-tracker). -
Set
PRODUCT_UIDin firmware. Openequipment_hours_tracker_helpers.hand replace the empty string on the#define PRODUCT_UID ""line with your value. You can also pass it as a build flag:-DPRODUCT_UID=\"com.your-company:tracker\". -
Claim the Notecard. Power up the assembled unit. The first time it reaches cellular or satellite, the Notecard claims itself to your project automatically, with no manual step. On cellular it shows up in the Devices tab within a few minutes. Give it longer on NTN while the Notecard sets up a satellite session, since Skylo availability depends on sky visibility and signal conditions, not on orbital pass timing.
-
Create a Fleet. Fleets group devices that share configuration. The natural units here are rental customer, equipment class, and geographic territory, so you might have a fleet for compact equipment, one for large iron, or one per region. Smart Fleet rules can auto-assign devices by tag. Tag units in Notehub and they'll move into the right customer or site fleet as their assignments change.
-
Set environment variables. In Notehub, go to Fleet → Environment (or Device → Environment to override a single unit). Every variable is optional, and the table shows the firmware defaults. The device pulls them on its next inbound sync (every 8 hours by default), with no reflash needed.
Variable Default Purpose vib_run_mg15.0RMS activity threshold in milli-g. Anything below it is classified as IDLE, whatever the CV. Lower it for smooth engines, and raise it in high-vibration environments to avoid false positives. vib_cv_max0.40Coefficient of variation ceiling for "engine running." CV below this value (steady vibration) → RUNNING, and CV above it (bursty vibration) → TRANSPORT. Engine idle usually has a CV of 0.1–0.25, while truck-bed bounce runs 0.5–1.0+. summary_interval_min1440Minutes between daily summary Notes. Changing it also re-applies hub.set outboundso the Notecard's sync cadence matches. Values outside 60–44640 min are ignored.geofence_lat0.0Latitude of the job-site ringfence center (decimal degrees, –90 to 90). Must be set together with geofence_lonandgeofence_radius_m. The default 0.0 means "not configured." A non-zero radius with lat/lon left at the 0,0 default won't turn on ringfencing, because the firmware only applies the ringfence once all three parameters are non-zero and in range.geofence_lon0.0Longitude of the job-site ringfence center (decimal degrees, –180 to 180). Must be set together with geofence_latandgeofence_radius_m.geofence_radius_m0Radius in meters. When all three ringfence parameters ( geofence_lat,geofence_lon,geofence_radius_m) are non-zero and in range, the Notecard watches the device's position against the ringfence center and triggers a_track.qoevent when the equipment leaves the site. The device picks up a change to any of the three values (even the radius alone) on its next wake and re-applies it. Non-zero values are clamped to 50–50,000 m. Setting it to 0 clears an active ringfence on the next wake, and does nothing if no ringfence was configured before. -
Configure routes. Add one route for
equip_event.qo(state-change events: low volume, immediate delivery to billing or dispatch) and a second forequip_summary.qo(rolling summary window Notes, delivered to a utilization dashboard or historian). A third route on_track.qohandles the ringfence-exit location breadcrumbs. Because the Notefiles are split at the source, each one can be routed with its own urgency, and the routes themselves need no filter logic.
What to expect in Notehub
-
_session.qo: automatic Notecard housekeeping on every cellular or satellite session. If you see it, the radio is reaching Notehub. If it's missing while the Notecard's LED shows activity, the usual cause is aPRODUCT_UIDmismatch or a coverage gap. -
equip_event.qo: one per state transition, sent right away. Theeventfield encodes the whole from→to transition, not just the new state:Transition eventtagsession_minIDLE / TRANSPORT → RUNNING engine_start0 RUNNING → TRANSPORT transport_startnon-zero, the duration of the engine run before transport IDLE → TRANSPORT transport_start0 RUNNING → IDLE engine_stopnon-zero, the duration of the run that just ended TRANSPORT → IDLE transport_stop0 Here's an example
engine_stopbody, which is one form of billing record. For RUNNING→TRANSPORT, seetransport_startwithsession_min > 0:{ "event": "engine_stop", "session_min": 94.5, "run_h_total": 1253.75, "epoch": 1746023400 } -
equip_summary.qo: one persummary_interval_min(1440 minutes by default). Each one covers a rolling window since the last report, not a calendar day. The first summary after boot may cover a shorter window if the Notecard's clock wasn't valid yet at startup. Body:{ "run_h": 6.2, "run_h_total": 1253.7, "transport_h": 1.1, "bat_v": 4.07, "fault_ct": 0 }A
bat_vbelow ~3.5V is a low-battery warning.transport_hgives you a second utilization metric, the time spent moving between sites.fault_ctcounts the state-change events dropped to event-queue overflow since the last summary. Anything non-zero means the Notecard was unreachable for several wakes in a row. -
_track.qo: the Notecard's automatic location heartbeat (every 4 hours) plus ringfence events. Firmware code doesn't generate these, because the Notecard's GPS subsystem owns them. Here's an example ringfence-exit event, from when the equipment leaves the configured job site:{ "status": "fence", "fence": "-_ring", "motion": 3, "time": 1746023400, "voltage": 3.98 }The key fields:
statusis"fence"only on ringfence transition Notes, which sets them apart from routine heartbeats.fenceis"-_ring"when the equipment left the ringfence and"+_ring"when it came back.inside_fenceistrueon any_track.qoNote recorded inside the ringfence and left out of Notes recorded outside it. Notehub appends the fix aswhere_lat/where_lon. To alert when equipment leaves the ringfence, usestatusandfencein a Notehub route. See_track.qofor details.
7. Firmware Design
The firmware is split across four files that have to live together in the same Arduino sketch folder:
equipment_hours_tracker.ino:setup()/loop()entry points and per-wake sequencing.equipment_hours_tracker_helpers.h: type definitions, constants, and function prototypes.equipment_hours_tracker_helpers.cpp: all the helper-function implementations.cx_sleep.h: host sleep for the Notecarrier CX, covering thecard.attnrequest, STOP2 entry, andATTN-pin wake.
The Arduino build tools compile every .ino, .h, and .cpp file in the
sketch folder together, so you don't need a manual include path or a Makefile.
7.1 Installing and flashing
Dependencies:
- Arduino core for STM32:
stm32duino/Arduino_Core_STM32. Install it from the Arduino Boards Manager (search for "STM32 MCU based boards"). The Notecarrier CX's host uses the Cygnet board definition (see the build settings below). Blues Wireless Notecard:note-arduino. Install it from the Arduino Library Manager (arduino-cli lib install "Blues Wireless Notecard"), or pick the latest stable version in the IDE Library Manager. The note-arduino releases page lists the available versions.Adafruit LSM6DS: install it from the Library Manager (arduino-cli lib install "Adafruit LSM6DS"). This also pulls in theAdafruit Unified Sensordependency if you don't have it yet.STM32duino Low Power(and its dependencySTM32duino RTC):stm32duino/STM32LowPower. Install it from the Library Manager (arduino-cli lib install "STM32duino Low Power" "STM32duino RTC"). It provides the STOP2 entry andATTN-pin wake thatcx_sleep.huses.
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 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).
Flashing with the Arduino IDE: Open equipment_hours_tracker.ino, apply
the build settings above, and click Upload. With the Notecarrier CX's USB-C
switch in HST, you can reach the host's DFU bootloader over the USB-C port
(hold BOOT, tap RST). You can also upload over SWD with an ST-LINK on the
debug jack.
Flashing with arduino-cli:
# One-time: install the STM32 board core
arduino-cli core install STMicroelectronics:stm32 \
--additional-urls https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json
# Libraries
arduino-cli lib install "Blues Wireless Notecard" "Adafruit LSM6DS" "STM32duino Low Power" "STM32duino RTC"
# Find your device's USB port
arduino-cli board list
# Compile and upload (replace port with your value from 'board list' above).
# usb=none disables the USB CDC stack, which must be off for the host to sleep.
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none \
firmware/equipment_hours_tracker/
arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none \
-p /dev/cu.usbmodem* \
firmware/equipment_hours_tracker/Replace /dev/cu.usbmodem* with the port that arduino-cli board list shows.
On Windows it's usually COMx, and on Linux it's /dev/ttyACM*.
firmware/equipment_hours_tracker/sketch.yaml sets the same board options, so
if you run arduino-cli compile in that directory without -b, it picks them
up too.
After flashing, open a serial monitor at 115200 baud on the ST-LINK virtual
COM port. You'll see [BOOT] Notecard configured at power-on, then a [VIB]
line on each 30-second wake with the live RMS and CV readings.
7.2 Modules
| Responsibility | Function |
|---|---|
Notecard one-time initialization (hub.set, card.location.*, template registration) | notecardConfigure, defineTemplates |
| Environment-variable fetch and hub.set re-apply | fetchEnvOverrides |
| Ringfence reconfiguration on env-var change | applyGeofenceIfChanged |
| Accelerometer burst sampling + RMS/CV classifier | classifyVibration |
| Hour-meter accumulation per state bucket | updateHourAccumulator |
| State-change event dequeue and delivery (Note.add + hub.sync, at-least-once retry) | sendNextPendingEvent |
| Daily summary emission | sendSummary |
| Time and voltage from Notecard | getEpoch, getBatteryVoltage |
Host sleep (STOP2 until the Notecard raises ATTN) | goToSleep → cxSleepUntilAttn (cx_sleep.h) |
7.3 Sensor reading strategy
On every wake, the host collects 208 accelerometer samples at 104 Hz (about 2 seconds of data) from the LSM6DSOX. For each sample it computes the 3-axis vector magnitude in m/s² and subtracts the 1g gravity baseline (~9.806 m/s²) to get the net dynamic acceleration. That residual is expressed in milli-g so it can be compared against the thresholds.
Then it computes two statistics over the 208-sample window:
- RMS: the root-mean-square of the residual magnitudes. A low RMS means the equipment is sitting still and undisturbed, and a high RMS means there's energy in the measurement.
- CV (coefficient of variation, σ/μ): the standard deviation normalized by the mean. A diesel engine idling at ~700 RPM produces ~11.7 Hz periodic vibration that's repetitive and steady in amplitude, so its CV is low (typically 0.10–0.25). Road shock through a truck chassis is aperiodic, with spikes at pothole crossings and relative quiet in between, so its CV is high (typically 0.50–1.0+). CV is what separates the two high-activity states.
Telling engine vibration apart from transport vibration was the main design challenge here. A simple amplitude threshold would fire on both. CV adds the second dimension that makes the problem solvable without training data or a heavy signal-processing stack.
7.4 Event payload design
Both Notefiles use compact templates. Notecard for Skylo requires them, because satellite data packets are capped at 256 bytes and every byte past the included 10 KB costs money. Compact templates store Notes on the Notecard as fixed-length binary records instead of free-form JSON, which cuts the wire size 3–5× compared to untemplated Notes.
equip_summary.qo (rolling summary window, queued):
{
"file": "equip_summary.qo",
"body": {
"run_h": 6.25,
"run_h_total": 1253.75,
"transport_h": 1.08,
"bat_v": 4.07,
"fault_ct": 0
}
}equip_event.qo (queued on each state transition, with a separate hub.sync
for prompt delivery):
{
"file": "equip_event.qo",
"body": {
"event": "engine_stop",
"session_min": 94.5,
"run_h_total": 1253.75,
"epoch": 1746023400
}
}The Notecard automatically attaches the coordinates of its last valid GPS fix to
both Note types, because the compact templates declare them with the
_lat/_lon metadata keywords. No firmware code handles coordinates in the
Note bodies.
7.5 Low-power strategy
Power matters on a solar-trickle-charged deployment, and the firmware pulls four levers to save it:
-
Host asleep between samples. After each sample cycle,
loop()callscxSleepUntilAttn()(incx_sleep.h). That function sends acard.attnsleeprequest forSAMPLE_INTERVAL_SECseconds, waits for the Notecard to pullATTNlow, and puts the STM32 host into STOP2, where it draws a few microamps and keeps its RAM. When the interval is up, the Notecard raisesATTN. The rising edge onD5(the jumper from §5) wakes the host, and execution picks up on the next line ofloop(). Since the host resumes in place instead of cold-booting, theAppStatestruct (hour meter, pending-event ring buffer, and ringfence cache) just stays in RAM. Nothing gets serialized to the Notecard, andsetup()runs only once per power-up. If the sleep request fails orATTNnever goes low (a missing jumper),loop()waits out the interval awake and tries again on the next cycle, so the 30-second cadence holds either way. -
Gyroscope shut down.
sox.setGyroDataRate(LSM6DS_RATE_SHUTDOWN)turns the gyroscope off right after init. This application doesn't need it, and shutting it down saves ~0.5 mA during the 2-second sampling window. -
Notecard sync decoupled from samples. The Notecard runs in
periodicmode with a daily outbound sync. Once the Notecard accepts a state-change event Note, the firmware sends a separatehub.syncto ask for prompt delivery outside the scheduled outbound window (usually 2–6 extra sessions per work day). Summaries queue up and go out in the daily outbound session. The firmware also sets an 8-hour inbound cadence (inbound: 480), so the Notecard checks Notehub for environment-variable updates three times a day. At default settings, that works out to roughly 4 radio sessions per day at minimum (1 outbound + 3 inbound), plus one more for each state-change event. -
GPS fix cadence. The firmware sets
card.location.modetoperiodicmode with a 900-second (15-minute) interval. In a typical deployment, GNSS fix attempts happen on or around that 15-minute cadence whenever the Notecard decides a new fix is needed. Each attempt usually draws 20–50 mA for 10–60 seconds. At up to 96 attempts a day, that can add roughly 15–30 mAh/day to the power budget, which is a real share of the total. If solar input is marginal or the device sits still much of the time, increaseGPS_PERIOD_SECONDSto cut GNSS power consumption.
Notecard for Skylo idles at about 8–18 µA @ 5V between sessions (see the low-power firmware design guide). An LTE-M session for a small queued payload lasts on the order of tens of seconds at ~100–300 mA peak. The Blues low-power guide measured Skylo NTN satellite sessions at roughly 27 mAh per 12-hour period with hourly syncs, and a daily sync cadence will use a lot less. On the bench, expect a device at default settings to total about 65–120 mAh per 24 hours, depending on network type, signal quality, how often the state changes, and how often GNSS fixes succeed.
7.6 Retry and error handling
- If
sox.begin_I2C()fails (an unplugged or miswired accelerometer), the firmware callsgoToSleep()right away instead of running with a broken sensor. The problem shows up in the serial monitor, and the firmware retries the IMU init on the next wake. cxSleepUntilAttn()returnsfalsewhen the Notecard doesn't accept thecard.attnrequest, or whenATTNnever goes low because theATTN→D5jumper is missing. In that casegoToSleep()waits out the 30-second interval awake withdelay()and tries again on the next cycle. Sampling keeps its normal cadence, at a much higher power cost.- The firmware checks the
errfield on bothcard.timeandcard.voltageresponses before trusting the returned value.getEpoch()returns 0 if the Notecard doesn't have valid time yet (no cellular/GPS sync), and all epoch-dependent logic gates onnow > 0.getBatteryVoltage()returns 0.0 on an error response. The summary Note carries that as a sentinel you can tell apart from a healthy ~3.6–4.2 V reading. - When
summary_interval_minchanges, the firmware ignores any value below 60 minutes and keeps the current interval. That keeps an operator from accidentally setting a 1-minute summary that would burn through the satellite data budget in hours. - State-change events aren't de-duplicated. If the classifier flips back and forth between RUNNING and TRANSPORT on rough terrain, every transition fires. If that causes alarm fatigue in a particular deployment, add a minimum-dwell counter as a production tuning step (for example, require 3 matching classifications in a row before accepting a new state).
7.7 Key code snippet 1: vibration classifier
The CV threshold is what lets the classifier tell an engine from transport. A 2-second burst at 104 Hz is fast enough to catch multiple engine-combustion cycles. A 700 RPM diesel fires every ~0.086 seconds, and 208 samples at 9.6 milliseconds spacing span ~23 combustion events.
const float n = (float)VIB_SAMPLE_COUNT;
float mean = sum / n;
float var = (sum_sq / n) - (mean * mean);
float cv = (mean > 1.0f) ? sqrtf(var > 0.0f ? var : 0.0f) / mean : 1.0f;
float rms = sqrtf(sum_sq / n);
// ...
if (rms < g_vib_run_mg)
return ST_IDLE;
return (cv < g_vib_cv_max) ? ST_RUNNING : ST_TRANSPORT;7.8 Key code snippet 2: compact template with GPS metadata
The _lat/_lon keywords in a compact template body tell the Notecard to embed
its most recent GPS fix in the Note automatically, so the note.add calls never
have to pass coordinates themselves.
How compact templates encode values (the numeric code parameter):
- 14.1 = IEEE 754 4-byte float (32-bit). The
.1just marks the field as a float, not a number of decimal places. Good for large dynamic ranges (hours, totals, coordinates). - 12.1 = IEEE 754 2-byte float (16-bit). Good for smaller ranges with moderate precision (voltage, relative measurements).
- 12 = 2-byte signed integer (16-bit), no decimal. Good for counters, small integers, or flags.
// equip_summary.qo — daily engine-hours summary
// 14.1 = 4-byte IEEE-754 float; 12.1 = 2-byte float; 12 = 2-byte integer
J *req = notecard.newRequest("note.template");
JAddStringToObject(req, "file", "equip_summary.qo");
JAddNumberToObject(req, "port", 50);
JAddStringToObject(req, "format", "compact");
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "run_h", 14.1);
JAddNumberToObject(body, "run_h_total", 14.1);
JAddNumberToObject(body, "transport_h", 14.1);
JAddNumberToObject(body, "bat_v", 12.1);
JAddNumberToObject(body, "fault_ct", 12); // event-queue overflow counter (2-byte int)
JAddNumberToObject(body, "_lat", 14.1);
JAddNumberToObject(body, "_lon", 14.1);
if (!checkedRequest(req))
return false;The Notecard compact template documentation has the full list of format codes and their ranges.
7.9 Key code snippet 3: immediate event with session duration
The event tag comes from both the previous and the new state, not just the
new one. That way engine_stop only ever marks a transition out of RUNNING
(the billing record), and transport_stop marks the end of a transport leg:
const char *tag;
if (new_state == ST_RUNNING) tag = "engine_start";
else if (new_state == ST_TRANSPORT) tag = "transport_start";
else /* ST_IDLE */ tag = (g_s.prev_state == ST_RUNNING)
? "engine_stop" : "transport_stop";session_min is non-zero on engine_stop (and on transport_start when the
previous state was RUNNING), and it records how long the engine ran since the
last engine_start. The first non-RUNNING transition uses up
run_session_start and clears it to zero. That's why any later non-RUNNING
transition (transport_stop, or any transition from a non-RUNNING prior state
like IDLE → TRANSPORT) always emits session_min = 0.
Each transition goes into a small ring buffer first, with its tag, epoch,
session_min, and run_h_total captured at that moment.
sendNextPendingEvent() sends the oldest record (e) with note.add (without
sync:true), making up to two attempts. Only once the Notecard accepts it does
the firmware send a separate hub.sync to ask for prompt delivery. That keeps
the network session decoupled from the Note acknowledgement:
for (int attempt = 0; attempt < 2; attempt++)
{
J *req = notecard.newRequest("note.add");
JAddStringToObject(req, "file", "equip_event.qo");
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "event", e.tag);
JAddNumberToObject(body, "session_min", e.session_min);
JAddNumberToObject(body, "run_h_total", e.run_h_total);
JAddNumberToObject(body, "epoch", (JNUMBER)e.epoch); // transition timestamp
J *rsp = notecard.requestAndResponse(req);
// ...
if (ok)
{
// ...
// Request prompt delivery as a separate fire-and-forget call so
// time-critical billing events are not held until the next
// scheduled outbound window. Delivery is ultimately the
// Notecard's responsibility; this call failing is non-fatal.
J *syncReq = notecard.newRequest("hub.sync");
J *syncRsp = notecard.requestAndResponse(syncReq);
if (syncRsp)
notecard.deleteResponse(syncRsp);
return true;
}
}7.10 Key code snippet 4: sleeping until the Notecard raises ATTN
cxSleepUntilAttn() asks the Notecard to hold ATTN low for exactly
SAMPLE_INTERVAL_SEC seconds, confirms the pin went low, and puts the STM32
into STOP2. Because the host stays alive, the firmware checks the result of the
card.attn request instead of firing it off and forgetting about it. The
Notecard's ATTN pin brings the host back on schedule, and loop() moves on to
the next sample.
setup() arms the ATTN wake once with cxSleepBegin(), and every loop()
pass ends in goToSleep():
// Arm the ATTN wake: the Notecard raises ATTN (D5) to end each sleep.
cxSleepBegin();
// ...
// Sleep in STOP2 until the Notecard raises ATTN SAMPLE_INTERVAL_SEC from now.
static void goToSleep(void) {
if (!cxSleepUntilAttn(notecard, SAMPLE_INTERVAL_SEC, NULL, &debugSerial)) {
// The Notecard didn't take the sleep request, or ATTN never went low
// (check the ATTN -> D5 jumper). Keep the sample cadence and retry.
debugSerial.println("[SLEEP] ATTN sleep failed — waiting out the interval awake");
delay(SAMPLE_INTERVAL_SEC * 1000UL);
}
}8. Data Flow
Collected on each 30-second wake: the 3-axis acceleration magnitude of each sample at 104 Hz for 2 seconds → RMS and CV → one of three equipment states (IDLE, RUNNING, or TRANSPORT).
Accumulated in host RAM (retained through STOP2): today's running hours, lifetime running hours, today's transport hours, and the session start timestamp.
Transmitted:
equip_event.qo: one Note per state transition. After the Notecard acknowledges the queued Note, the firmware sends ahub.syncrequest so it goes out ahead of the scheduled outbound window. Expect 2–6 of these per work day (engine start, maybe a midday idle, engine stop, plus transport start/stop on delivery days). On cellular they reach Notehub as soon as a session comes up (~15–60 seconds). On NTN they go out once satellite service is available, which depends on sky visibility and session setup and can take longer than cellular.equip_summary.qo: one Note persummary_interval_min(1440 minutes by default), queued and shipped in the Notecard's next outbound session. It covers the rolling window since the last report, not a calendar day, and the first one after boot may cover only part of a window if the Notecard's clock wasn't valid at startup. It carries the window's run hours, the lifetime total, transport hours, battery voltage, and a fault counter (fault_ct) for any event-queue overflows since the last summary._track.qo: the Notecard's GPS subsystem emits these on its own, every 4 hours as a heartbeat location record and on ringfence exit ifgeofence_radius_mis set. The firmware doesn't generate them directly.
Routed. Both application Notefiles go to Notehub, then on to whatever
downstream endpoints the project's routes name. A typical setup sends
equip_event.qo to a billing/dispatch system or a CMMS (computerized
maintenance management system) webhook, equip_summary.qo to a time-series
database for trending and predictive maintenance scheduling, and _track.qo to
a mapping/GIS layer.
Alert triggers:
engine_start: a rental company watching for unauthorized after-hours use can act on this right away.- Any
equip_event.qowithsession_min > 0: a run session just closed. Theeventtag tells you what came next.engine_stopmeans the machine ran and then went idle, andtransport_startmeans it ran and then started moving (RUNNING → TRANSPORT). Both carry the run duration insession_min. Billing systems must key offsession_min > 0across both tags, because keying only offengine_stopmisses completed run sessions that end in transport. transport_start: the equipment is moving. It's useful for confirming scheduled deliveries or catching unplanned moves. A non-zerosession_minmeans the engine was running right before transport began.transport_stop: a transit leg ended. Paired with the precedingtransport_starttimestamp, it gives you the transit duration for dispatch and mileage tracking.- Battery voltage below 3.5V in
equip_summary.qo: solar input isn't keeping up at this site, so move the panel or add capacity. - No
equip_event.qofor several days while_track.qoshows a steady position: the equipment may be sitting idle on a lot, which makes it a candidate for redeployment or servicing.
9. Validation and Testing
Expected steady-state on an active job site. In normal operation, expect 2–4
equip_event.qo Notes a day (engine start, engine stop, maybe a midday
shutdown) and one equip_summary.qo per summary window (every 24 hours by
default). You'll also see six _track.qo location heartbeats a day (one every 4
hours, matching GPS_HEARTBEAT_HOURS = 4), plus any extra _track.qo Notes
that ringfence events trigger. On a delivery day, extra transport_start and
transport_stop events bracket the trip. If the engine was running right before
transport, the transport_start Note carries a non-zero session_min with the
run duration.
Bench validation. Sitting on a desk, the equipment is IDLE. The
accelerometer should report a low RMS, the classifier should output ST_IDLE,
and the serial monitor shows [VIB] rms=X.X mg cv=Y.YYY → IDLE. To simulate an
engine:
- Tap the enclosure rhythmically against a surface at ~10 Hz. Don't keep a
perfect beat. Vary the interval a little to mimic the irregularity of engine
combustion. The CV should drop below 0.40 and the classifier should output
ST_RUNNING. - Shake the enclosure sharply, then at random (slap → pause → slap → pause).
That models road bounce, so you should see a high CV and
ST_TRANSPORT.
Tuning the classifier on your equipment. If the classifier misfires at the default thresholds (false RUNNING during transport, or idle going undetected):
- Tap the enclosure in the three states and write down the RMS and CV values the serial monitor prints.
- If idle is triggering as RUNNING, raise
vib_run_mga little (try 18.0 or 20.0) to lift the activity floor. - If transport is triggering as RUNNING, lower
vib_cv_max(try 0.35 or 0.30) to tighten what counts as "engine-like vibration." - If engine start is missed entirely, lower
vib_run_mg(try 12.0 or 10.0) or raisevib_cv_max(try 0.45).
Update the environment variables in Notehub (Fleet → Environment). The
device pulls the new thresholds on its next inbound sync (~8 hours by default,
or right away if you trigger a manual sync in Notehub). On the next wake, check
that the new values show up in the serial output, where the firmware logs
[ENV] vib_run_mg=X.X vib_cv_max=Y.YY whenever either threshold changes. Keep iterating on live equipment until
classification is reliable for your engine type and chassis.
Power validation with Mojo. The Mojo reports
cumulative mAh over its Qwiic link. For bench measurements, Mojo takes the place
of the normal battery feed. Leave the LiPo JST unplugged and power the
carrier's VBAT pad only through the Mojo: bench supply (or the LiPo with its
JST lead brought out directly) → Mojo BAT input → Mojo LOAD output →
Notecarrier CX VBAT pad. Disconnect the solar panel too. The Notecarrier
CX's solar charger input is a separate rail. If it stays connected during bench
testing, solar current offsets the load draw, so you under-measure the real
consumption and can't repeat the measurement under different light. Once the
LiPo JST and the solar panel are both disconnected and all power runs through
the Mojo, every bit of load current passes through its coulomb counter and the
reading reflects the true whole-device draw.
Important measurement scope. The Mojo is spliced into the main power rail
(LiPo → Notecarrier CX VBAT), so it measures the whole-device subsystem:
Notecard for Skylo, the Notecarrier CX's onboard regulators and solar charger,
and the LSM6DSOX IMU. It doesn't measure the Notecard alone. The published
Notecard idle figure (~8–18 µA @ 5V) applies to the Notecard's own power domain
by itself, and the Notecarrier CX adds its own quiescent current from the
onboard power management IC and charger. Treat the Mojo figures below as a
realistic whole-system deployment budget, not something to compare directly
against a datasheet. To isolate the Notecard's draw, you'd have to separate its
VMODEM rail from the carrier board. The Notecard low-power design
guide explains how
to isolate the rail.
Here's the expected current profile at default settings (30-second sample interval, 1 daily outbound + 3 inbound sessions):
| Phase | Expected current (whole-device rail) |
|---|---|
| Host in STOP2 between samples (Notecard + carrier idle) | ~20–60 µA @ 5V (Notecard idle + host STOP2 ~1–2 µA + carrier quiescent) |
| Host awake + accelerometer sampling (~2–3 seconds) | ~5–15 mA |
| GNSS fix attempt (up to every 15 minutes, 10–60 seconds duration) | ~20–50 mA |
| Notecard cellular session (LTE-M, small payload) | ~100–300 mA average, brief bursts |
| Notecard NTN session (satellite, small payload) | ~150–400 mA average during transmission window |
At default settings, a healthy Mojo trace looks like this:
- Flat near-zero baseline (~20–60 µA) between wakes: Notecard idle plus the Notecarrier CX quiescent draw while the host sits in STOP2.
- Brief ~5–15 mA blip every 30 seconds, ~2–3 seconds long: the host waking
on
ATTN, running the IMU sample burst, and classifying vibration before it callsgoToSleep(). - ~20–50 mA pulses at up to 15-minute intervals, 10–60 seconds long: the Notecard GNSS module getting a location fix. It's in periodic mode, so the real cadence may be lower when the device is stationary.
- Three scheduled inbound sessions per day: every 8 hours the Notecard
checks in with Notehub briefly to pull environment-variable updates
(
inbound: 480), usually for 10–30 seconds on LTE-M. There's also one daily outbound session that carries the queuedequip_summary.qo(usually 10–60 seconds on LTE-M). On NTN, both inbound and outbound sessions can take longer, depending on satellite availability. - Additional radio sessions for each state-change event: once the Notecard
accepts an
equip_event.qoNote, the firmware sends ahub.syncrequest that triggers an immediate sync outside the scheduled cadence. A typical active work day has 2–6 state-change events (engine start, stop, maybe some midday transport legs), so expect 2–6 sessions on top of the baseline. All told, that's roughly 6–10 total sessions per active work day at default settings. - Ringfence-triggered transmissions (if
geofence_radius_mis set): the Notecard emits a_track.qoon its own when the equipment exits the ringfence, and syncs it right away.
If the baseline sits at ~5–10 mA the whole time, the host isn't entering STOP2.
Make sure the ATTN → D5 jumper from §5 is physically there and seated, and
that the firmware was built with USB support set to None (usb=none). The
serial output logs [SLEEP] ATTN sleep failed whenever cxSleepUntilAttn()
can't sleep. If a cellular or NTN session runs unusually long (>60 seconds on
LTE-M), the radio is fighting poor signal. Check the MAIN antenna placement,
make sure it has an unobstructed view of the sky, and confirm it's the
Skylo-certified unit that ships with the NOTE-NBGLWX.
Solar viability estimate. The Mojo sits on the load rail, so it only
measures consumption. It can't see current flowing through the Notecarrier CX's
separate solar charger input. With the solar panel disconnected and the unit
running from a known LiPo or bench supply, run the Mojo for a full 24 hours and
write down the total mAh consumed. Then compare that number against the
theoretical harvest for your panel size and site. A 1W panel with 4 effective
sun-hours produces 4 Wh (4000 mWh) raw. After typical derating for panel
temperature and incidence angle (~80%), charger conversion efficiency (~85%),
and soiling (~90%), the usable harvest is roughly 2.4 Wh, or about 480 mAh at
5V. At default settings the GPS cadence alone adds ~15–30 mAh/day. Add host
wakes, three daily inbound check-ins, one outbound sync, and the usual 2–6 event
sessions on active days, and whole-device consumption comes to about
65–120 mAh/day. On cellular, a 1W panel with ≥3 effective sun-hours a day
usually covers that. Satellite sessions each draw more, so size the panel for
each site. If Mojo shows a growing deficit across repeated 24-hour tests (solar
disconnected), consider a 2–5 W panel, a higher-capacity LiPo, or a longer GPS
period (GPS_PERIOD_SECONDS). To check the real panel and charger harvest
instead of relying on the derating model, put a DC current meter inline on the
solar cable itself and log it over a representative sunny day.
10. Troubleshooting
| Symptom | Likely Cause | Solution |
|---|---|---|
| Serial monitor shows no output after upload | Monitoring the wrong port, or board options not applied | Debug output goes to the LPUART on the Notecarrier CX debug jack, not USB, so open the ST-LINK virtual COM port at 115200 baud (see §7.1). Check that Blues boards → Cygnet is selected with USB support set to None. On Windows, install the ST-Link drivers. |
[IMU] Not found message | Accelerometer not powered or miswired | Check the I2C connections (SDA, SCL, +3V3, GND) and run an I2C scanner sketch to make sure the LSM6DSOX answers. See §5. |
No _session.qo appearing in Notehub after several minutes | PRODUCT_UID mismatch, missing antenna, or no cellular/NTN coverage | Check that PRODUCT_UID matches the value you created in Notehub (copy and paste it from Notehub → your project settings). Confirm the MAIN antenna is mounted outside with an unobstructed sky view. Watch the Notecard's LED, which should flash briefly during sync attempts. On satellite, allow 5+ minutes for Skylo to set up a session, depending on sky visibility. |
Classifier always outputs IDLE even when tapping | Thresholds too high | Lower vib_run_mg to 10.0 (in Notehub Fleet → Environment), then wait for the next inbound sync (~8 hours) or trigger a device fetch manually. |
Classifier falsely triggers RUNNING during idle periods | Thresholds too low | Raise vib_run_mg to 20.0 and lower vib_cv_max to 0.30. |
[SLEEP] ATTN sleep failed in the log, or baseline current 5–10 mA (host never sleeping) | ATTN → D5 jumper not installed or miswired, or firmware built with USB CDC enabled | Check that the jumper physically connects the ATTN and D5 pins on the Notecarrier CX header (see the "ATTN → D5 jumper" line in §5), and use a multimeter to confirm continuity. Make sure the firmware was built with USB support set to None (usb=none), because with USB CDC enabled the host leaves STOP2 within a millisecond. If either one is wrong, the host runs continuously and the power budget falls apart. |
Battery voltage in equip_summary.qo declining over days | Solar input inadequate | Make sure the 6V solar panel gets enough direct sunlight (≥3 effective sun-hours per day in temperate climates). If the site is shaded, or at high latitude in winter, upgrade to a 2–5 W panel. Put a DC meter inline on the solar cable to confirm the real harvest. |
Duplicate equip_event.qo Notes in Notehub | I2C acknowledgement lost after Notecard accepted Note | This edge case is by design (at-least-once delivery). Downstream routes should dedupe on the epoch + event pair, since the Note's timestamp and event type are unique for each transition. |
| Arduino-cli reports "board not found" or an invalid board option | Core not installed or board name wrong | Run arduino-cli core list to confirm STMicroelectronics:stm32 is installed. If it isn't, run the full core install command from §7.1. Check that the board options are exactly STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none (they're case-sensitive). |
11. Limitations and Next Steps
This is a reference build, not a finished fleet product. A few things are scoped down on purpose so the core idea (engine-hour billing from a magnetic stick-on with no wiring) stays easy to follow. Below are the places you'll want to harden before this goes on a thousand machines, followed by the natural production extensions.
Simplified for this POC
These are the corners this build cuts on purpose, each with what you'd do differently on a production fleet.
The vibration classifier is heuristic, not trained. The RMS + CV algorithm
does a good job of telling engine idle from transport vibration on diesel
construction equipment at typical idle RPMs. Gasoline-powered equipment idles
more smoothly, though, so its CV can come in lower and may overlap with
transport at certain speeds. On very rough terrain, engine-running CV can also
creep above the default threshold. The vib_run_mg and vib_cv_max
environment variables are the tuning knobs, but dialing them in precisely
requires per-equipment-class calibration from logged data. A production
deployment would instrument a representative sample of each equipment type, log
raw RMS/CV values over several shifts, and work out a threshold pair for each
class.
Hour accumulation granularity is 30 seconds. Each wake credits the time
since the previous wake (from card.time, capped at three sample intervals) to
the running bucket if the previous state was RUNNING. Before the Notecard has
valid time, it credits a nominal SAMPLE_INTERVAL_SEC instead. An engine that starts halfway through a 30-second sleep interval
gets picked up on the next wake, so the worst-case rounding error is one
sample interval (30 seconds). That's usually fine for billing. If you need
sub-minute precision, drop SAMPLE_INTERVAL_SEC to 10, which costs you ~3× as
many host wakes.
The first run session after power-on may report session_min: 0 if the
Notecard doesn't have valid time yet. getEpoch() returns 0 when card.time
has no cellular or GPS sync, and the firmware refuses to record a start
timestamp of 0. So if the device goes to RUNNING before time is valid,
run_session_start stays at 0, and the closing engine_stop or
transport_start event carries session_min: 0. That makes it useless as a
standalone billing record. Once the Notecard has time, later sessions compute
correctly. During the affected cold-start window, the daily equip_summary.qo
still adds up run hours in the hour-meter buckets regardless of session
boundaries, so use the summary to reconcile any gap. Billing integrations
should treat a session_min: 0 on the first event after a device power cycle
as an incomplete record.
The hour counter is software-based and lives in host RAM. The run_h_total
field in AppState survives every STOP2 sleep/wake cycle, but nothing writes it
to non-volatile storage. A power cycle, a reset, or a firmware update resets
the lifetime total to zero. A production version should keep the
authoritative total server-side in Notehub (for example, as a device-level
environment variable updated on each engine_stop event) so it survives a
hardware replacement.
There is no ringfence alerting in the application firmware. The Notecard
handles ringfence exit on its own (_track.qo), and the application firmware
only sets the ringfence center through card.location.mode. The _track.qo
Notes that result carry location data but no application-level label. A
production deployment should add a Notehub route that fires an alert when a
_track.qo shows up outside the ringfence.
The ringfence cannot be centered at the equator or prime meridian. The
firmware treats geofence_lat = 0.0 and geofence_lon = 0.0 as the "not
configured" sentinel. It refuses to apply a ringfence if either coordinate is
within ≈0.0001° of zero (the check is fabsf(lat) > 0.0001f and
fabsf(lon) > 0.0001f). That means a site sitting right on the equator
(lat ≈ 0°) or the prime meridian (lon ≈ 0°), say in southern Ghana, the
Republic of Congo, or the English Channel, can't use the ringfence feature as
it's written today. To lift the restriction, replace the 0,0 sentinel with an explicit enable
flag (for example, a geofence_enable environment variable set to 1) and let
lat/lon take any in-range value, zero included.
Solar panel sizing is for temperate climates. A 1W panel + 2000 mAh LiPo
gives enough runtime in most regions with ≥3 effective sun-hours per day. At
higher latitudes in winter, or with the enclosure on a shaded part of the
chassis, you may need a larger panel (2–5W) or a higher-capacity LiPo
(5000 mAh). The bat_v field in equip_summary.qo is your early warning. A
voltage that keeps dropping over several days means the panel isn't harvesting
enough.
Mojo is not read in firmware. The firmware doesn't poll the Mojo's LTC2959
coulomb counter over Qwiic. If the operator wants fleet-level energy telemetry,
adding a mojo_mah field to equip_summary.qo is a simple extension.
Production Next Steps
Taking the tracker to a real fleet usually starts with tuning it on real data, then moves on to over-the-air updates and more sensing.
Per-equipment-class threshold calibration means shipping a "learning mode" firmware build that logs raw RMS/CV data at 1-minute intervals for several shifts, then switches over to production classification.
Lifetime hour counter persistence in Notehub environment variables would keep the running total intact when a device gets replaced.
A ringfence-exit alert route in Notehub would fire a webhook when a
_track.qo Note shows up with a location outside the configured ringfence, so
you'd hear about unauthorized moves.
Over-the-air firmware updates to the Notecarrier CX's STM32 host through Notecard Outboard DFU would let you ship threshold algorithm improvements and new features without sending a technician to every machine.
Tamper detection would pick out an abrupt, very high-amplitude single-axis
spike (like the magnet base being pulled off) from normal equipment vibration
and flag it as a tamper event.
Multi-sensor fusion would pair the accelerometer with an engine temperature sensor (an NTC thermistor on the exhaust manifold) or a current clamp on the alternator output. That gives you a second, independent check on engine state and makes the classifier more reliable on unusual equipment types.
12. Summary
The rental excavator from the start of this story now reports its own hours. A
magnetic enclosure goes onto the frame rail in under ten minutes, without
drilling, a wiring harness, or help from the OEM. From then on, the machine
sends engine hours, location, and work-session events to Notehub over whatever
network it can reach. The two-parameter RMS + coefficient-of-variation
classifier is what makes vibration-only detection trustworthy. It can tell a
diesel idle from a flatbed delivery without any wiring to the engine, and
Notecard for Skylo keeps the data moving whether the machine is in a regional
yard or at the bottom of a quarry. On the rental company's side, the data is
simple to use. Any equip_event.qo with session_min > 0 is a billing record,
run_h_total drives maintenance scheduling, and a transport_start at 2 AM is
the unauthorized-use alert. The whole fleet runs the same firmware and the same
hardware out of one Notehub project.
On This Page
- 1. Project Overview
- 2. System Architecture
- 3. Technical Summary
- 4. Hardware Requirements
- 5. Wiring and Assembly
- 6. Notehub Setup
- 7. Firmware Design
- 7.1 Installing and flashing
- 7.2 Modules
- 7.3 Sensor reading strategy
- 7.4 Event payload design
- 7.5 Low-power strategy
- 7.6 Retry and error handling
- 7.7 Key code snippet 1: vibration classifier
- 7.8 Key code snippet 2: compact template with GPS metadata
- 7.9 Key code snippet 3: immediate event with session duration
- 7.10 Key code snippet 4: sleeping until the Notecard raises ATTN
- 8. Data Flow
- 9. Validation and Testing
- 10. Troubleshooting
- 11. Limitations and Next Steps
- 12. Summary