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

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

Construction Equipment Anti-Theft Tracker with Immobilizer

Construction Equipment Anti-Theft Tracker with Immobilizer banner
note

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

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

This project is a loss prevention reference design for construction equipment fleets. Fit it to a skid steer, light tower, portable compressor, or generator and the machine becomes a much harder target. It reports its own location continuously, sends an alert when it breaches its geofence or moves after hours, and accepts an ignition immobilizer command that you stage remotely. It runs on its own cellular-plus-satellite link and a small battery that a solar panel keeps topped up, so you don't need job-site WiFi or any help from IT.

1. Project Overview

The problem. Construction equipment theft is a serious, expensive problem, and it's chronically underreported. A skid steer that costs $60,000 to replace can be driven onto a flatbed and hauled off in under ten minutes, and a light tower or portable generator can be gone before the morning crew shows up. Nobody smashes and grabs a skid steer. The equipment is large and easy to identify, and it sits completely exposed on a site that often goes unwatched overnight. Law enforcement recovers relatively little stolen heavy equipment. The unit often ends up at a rural stash yard, gets stripped, or gets loaded into a shipping container for export. Those are exactly the places where cellular telemetry that depends on urban coverage goes dark.

So the industry leans heavily on equipment marking, insurance claims, and police reports filed after the theft. What's mostly missing is a device that reports real-time location while a theft is in progress and can physically disable the equipment before it leaves the region, without depending on infrastructure that may not exist at the job site or at the final stash location.

This project builds that device. It checks GPS location against a configurable job-site geofence and watches for unexpected motion after hours. It also accepts a remote immobilizer command from the Blues Notehub cloud service, which stages a relay to cut the ignition circuit the next time the thief turns the key. The immobilizer is a polled, staged proof of concept rather than a continuously held cut, and §11 covers the limits that come with that. GPS, cellular, and satellite fallback all live in a single Blues Notecard for Skylo. It sits in a weatherproof enclosure and runs indefinitely on a LiPo battery that a small solar panel keeps topped off.

Why Notecard. Construction sites have no WiFi. Even when a site happens to have a hotspot, a security device that needs site IT to get online defeats the purpose. Cellular works in most scenarios, but a stolen skid steer doesn't stay parked somewhere with good urban coverage. It ends up in a rural equipment yard, a metal barn, or a shipping container at a port, where cellular alone fails and satellite decides whether you recover the asset or write it off. Notecard for Skylo (NOTE-NBGLWX) handles all three radio paths in a single M.2 module, with no firmware branching: LTE-M/NB-IoT cellular, Skylo satellite, and WiFi where it's available. For this use case, global roaming and satellite failover are the core of the design rather than add-ons.

New to Blues?

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

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

Deployment scenario. A NEMA 4X weatherproof enclosure hides inside the equipment housing or mounts to a structural member where nobody will see it. It carries its own power, a LiPo battery that a small rooftop solar panel keeps topped off, so the tracker keeps running even after a thief disconnects the equipment's main battery. Two wires tap the ignition circuit. One reads the ignition-on state through a voltage divider, and the other runs through the relay that can cut the starter circuit on command. The antenna leads run to externally mounted elements with a clear view of the sky.

2. System Architecture

System architecture: equipment signals → Notecarrier CX with STM32 host and Notecard for Skylo → LTE-M/NB-IoT + Skylo satellite → Notehub → security and fleet-management routes

Device-side responsibilities. The STM32 host in the Notecarrier CX spends almost all of its time asleep, and the whole power budget depends on that. When the configurable wake timer fires, the firmware reads the ignition state from a voltage divider on the 12 V ignition line, polls the Notecard's built-in accelerometer with card.motion, and pulls the latest GPS fix with card.location. Then it runs three checks in order: whether the position is outside the job-site geofence (evaluated with a Haversine distance), whether it's the after-hours window, and whether a fresh immobilize command is waiting in the inbound queue. Heartbeat and alert Notes go to the Notecard over I²C through the note-arduino request helpers. Between wakes the host sleeps in STM32 STOP2 (a few µA) until the Notecard's ATTN pin wakes it. Because the host resumes in place with its RAM intact, the runtime state (geofence center, immobilizer stage, and cadence parameters) just stays in the AppState struct. Only the geofence also gets saved to the Notecard (fence.db), so it survives a full power loss.

Notecard responsibilities. Notecard for Skylo does most of the connectivity work behind the scenes. It buffers queued Notes in on-device flash and opens a cellular or satellite session on the hub.set outbound cadence. Any sync:true alert Note goes up right away, and that's the difference between an alert reaching the operator in seconds and the equipment crossing a county line. The same module also has the GPS receiver, the accelerometer, the real-time clock, and the battery-voltage ADC, so you don't need any external sensors. Environment variables come down from Notehub on each inbound sync. A fleet manager can retune the geofence radius or the after-hours window from a browser, and the firmware reissues hub.set and card.location.mode on its own so all three Notecard cadences stay matched to the new wake interval.

Notehub responsibilities. Notehub ingests and stores every event from every device, whether it came in over cellular or Skylo, and applies the project's routes. Alert Notes (alert.qo) and tracker heartbeats (tracker.qo) land in separate Notefiles. One device can then serve both the security platform that needs alerts the moment they happen and the fleet-management store that needs position history, with no filter logic in between. To stage an immobilize or release command, an operator posts a Note to the device's immobilize.qi inbound queue from the Notehub UI or through the Notehub REST API. Smart Fleets push site-level geofence and after-hours defaults to every machine on a job site at once.

Routing to the cloud (high level). Notehub can send data to HTTP, MQTT, AWS IoT, Azure, GCP, Snowflake, and other destinations. Route setup depends on your project, and this one doesn't ship with a specific downstream endpoint. The Notehub routing docs cover the options.

3. Technical Summary

What you'll have when done:

  • A powered device, claimed in Notehub and showing its real GPS location
  • Real-time geofence alerts when the device moves outside a job-site boundary
  • A way to stage an immobilizer command from the Notehub UI and confirm it fires on the next ignition key-on
  • Confidence in how the firmware and sensors behave before you wire anything into live equipment

Fastest path to first event (bench test, no ignition circuit yet):

  1. Assemble the unit. Insert Notecard for Skylo into the Notecarrier CX's M.2 slot and connect the LiPo battery to the Notecarrier CX's JST battery header. Route the antenna cables with some slack (MAIN u.FL → Skylo-certified antenna, GPS u.FL → passive GNSS patch), because you'll finalize their placement once the enclosure location is set. Mount everything in a NEMA 4X weatherproof box and power the Notecarrier CX over USB.

  2. Get your ProductUID. Sign up at notehub.io, create a new project, and copy the ProductUID from the project settings.

  3. Flash the firmware.

    • Install the Arduino IDE or arduino-cli.
    • In the Arduino Boards Manager, install the STM32 core. The build targets the Cygnet board definition with USB support set to None (see §7).
    • In the Arduino Library Manager, install "Blues Wireless Notecard" and "STM32duino Low Power" (plus its dependency "STM32duino RTC").
    • Open firmware/construction_equipment_anti_theft/construction_equipment_anti_theft.ino.
    • Paste your ProductUID into the PRODUCT_UID string at the top of the sketch.
    • Fit the ATTN-to-D5 jumper on the Notecarrier CX header (§5), then flash the Notecarrier CX.

    Via arduino-cli: The board options below match firmware/construction_equipment_anti_theft/sketch.yaml, which arduino-cli picks up on its own when you run it from the sketch directory. usb=none turns off the USB CDC stack, which has to be off for the host to sleep:

    arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC"
    arduino-cli compile --fqbn "STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none" \
      firmware/construction_equipment_anti_theft
    arduino-cli upload --port /dev/ttyACM0 --fqbn "STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none" \
      firmware/construction_equipment_anti_theft
  4. Claim the device. Power up the unit and leave it powered. On first boot the Notecard connects over cellular and auto-provisions to your project. In Notehub, go to your project → Devices and confirm the Notecarrier CX shows up in the device list within 30 seconds.

  5. Watch the heartbeat. Click the device in Notehub and open the Events tab. The firmware queues a tracker.qo heartbeat Note on its first wake, with the current lat, lon, and battery voltage. Heartbeats go out in a batch on the outbound schedule (every 4 hours by default, outbound_min), so the first one can take up to 4 hours to show up. If nothing shows up, check the USB power and the antenna placement, since Skylo lock needs a clear view of the sky.

  6. Test the geofence. In Notehub's Fleet view, set these environment variables:

    • fence_enabled = 1
    • fence_lat = your current latitude (shown in the device event detail)
    • fence_lon = your current longitude
    • fence_radius_m = 20 (20-meter test radius)

    Give the inbound sync 4–5 minutes after hours (up to 60 minutes while parked during the day) to deliver these variables to the device. Then physically move the device ~25 meters away and watch the Events log for a geofence_breach alert, which should appear within 1–2 wake cycles. During business hours (6 AM–6 PM UTC by default) the device only wakes every 60 minutes, so test in the after-hours window (6 PM–6 AM) to get the faster 2-minute wake cycles.

  7. Test immobilizer staging. In the Notehub device view, use the command bar to post a Note to immobilize.qi:

    {"cmd":"immobilize"}

    Watch the Events log for immobilize_armed, which confirms the device received and staged the command. Next, simulate a key-on edge. Connect the host's A2 GPIO to GND (ignition OFF state), leave it there for one full wake cycle, then briefly short A2 to 3.3V (ignition ON). On the next wake after the OFF→ON transition, the relay driver pin A1 should pulse HIGH and ignition_on_immobilized should show up in the Events log. That confirms the immobilizer path works before you do any live wiring.

Here's a sample Note this device emits:

{
  "file": "tracker.qo",
  "body": {
    "lat":         37.3387,
    "lon":       -121.8853,
    "loc_valid":   1,
    "ignition":    0,
    "moving":      0,
    "immobilized": 0,
    "bat_v":       3.92,
    "fence_ok":    1,
    "fix_age_s":   47.0
  }
}

4. Hardware Requirements

warning

12 V systems only. Everything in this reference design, from the voltage-divider ratios to the relay coil rating and the wiring guidance, is engineered for 12 V electrical systems. Most compact construction equipment (skid steers, portable generators, light towers) ships with a 12 V system, but larger machines (heavy excavators, some European platforms) use 24 V. On a 24 V ignition rail, the 33 kΩ / 10 kΩ ignition-sense divider shown here would put about 5.6 V on the host's A2 GPIO, which is over the 3.3 V limit, and the 12 V relay coil would overheat. Verify your equipment's system voltage before proceeding. Adapting the design to 24 V takes different divider values (e.g. 68 kΩ high-side + 10 kΩ low-side → 24 V × 10/78 ≈ 3.1 V, within the GPIO limit) and a 24 V–rated relay coil.

PartQtyRationale
Notecarrier CX (datasheet)1Compact carrier with an embedded STM32L433 host MCU, so you don't need a separate Swan or Feather. The host sleeps in STOP2 between wakes, and the Notecard's ATTN pin (jumpered to D5) wakes it through card.attn.
Notecard for Skylo (NOTE-NBGLWX) (datasheet)1LTE-M/NB-IoT/GPRS cellular, Skylo satellite, and WiFi, all in a single M.2 module, with automatic cellular-to-satellite failover and no firmware changes. It ships with its Skylo-certified MAIN cellular/satellite antenna (see the antenna row below) and includes 500 MB of cellular data and 10 KB of bundled Skylo data.
Blues Mojo (datasheet)1Bench-only coulomb counter for checking energy use during commissioning. It sits inline on the +VBAT rail and measures current draw across the sleep, active, and transmit phases. It isn't deployed in the field (see §11).
3.7 V LiPo battery, 2000 mAh, JST-PH 2.0 mm (e.g. Adafruit 2011, or equivalent 2000–4000 mAh cell)1Primary energy storage. Size the capacity for the solar you expect to get and the dark-sky reserve you want, using the phase-by-phase current-draw breakdown in §9.
6 V, 10 W solar panel, ~110×70 mm (e.g. Voltaic Systems P110)1Trickle-charges the LiPo during daylight hours. Construction equipment usually lives outdoors, and the 10 W panel leaves plenty of headroom even at sites with limited direct sun or long overcast stretches.
Solar LiPo charger module, 3.7 V LiPo output (e.g. SparkFun Sunny Buddy PRT-12885, or equivalent MPPT/CV-mode charger)1Sits between the solar panel and the LiPo and protects the battery from overcharge. Its output connects to the LiPo JST input on the Notecarrier CX, so check that the charger's output connector matches the Notecarrier CX's battery header.
BSS138 logic-level N-channel MOSFET, SOT-23 (e.g. Nexperia BSS138 or equivalent with R_DS(on) ≤ 3.5 Ω at V_GS = 2.5 V and I_D ≥ 300 mA)1Low-side relay coil driver. For a 3.3 V logic-level gate drive on a security-critical path, choose the part by R_DS(on) at V_GS = 2.5 V, not by threshold voltage. The Nexperia BSS138 datasheet specifies R_DS(on) max 3.5 Ω at V_GS = 2.5 V, I_D = 100 mA. At the relay coil current (~150–175 mA for a typical ISO 280 coil), worst-case V_DS ≤ 175 mA × 3.5 Ω ≈ 0.61 V and P_D ≈ 0.11 W, which is within the SOT-23 thermal rating. The relay coil sees ≥ 11.4 V of the 12 V supply, well above its pull-in threshold. Drain goes to relay coil −, and source goes to GND. The 10 kΩ gate-to-source pulldown keeps the MOSFET off when the GPIO is high-impedance during reset.
Automotive SPDT relay, 12 V coil, 30 A contacts (ISO 280 mini relay, e.g. Bosch 0 332 019 150 or equivalent Tyco/TE V23134-J52-X255)1Wired in series with the equipment's starter solenoid enable or ignition/run-start control circuit. That's the low-current signal wire that authorizes cranking, not the high-current cable that feeds the starter motor itself. Energizing the coil opens the normally-closed contact, so the engine can't crank. Any automotive supplier or electronics distributor carries them.
Inline automotive fuse holder with 1 A or 2 A mini (ATC/ATO blade) fuse1Goes on the wire that feeds the relay coil + terminal from the 12 V battery or ignition tap, as close to the power source as practicable. A typical ISO 280 coil draws ~150–175 mA, so a 1 A fuse gives ~6× margin over the continuous coil current while still protecting the added harness from a short-circuit fault. Don't rely on the equipment's upstream fusing. Any newly added conductor tied to battery or ignition power should carry its own fuse near the source.
1N4007 diode1Flyback protection across the relay coil. It protects the BSS138 MOSFET drain from the inductive spike when the coil de-energizes, so don't leave it out.
10 kΩ resistor, ¼ W1Gate-to-source pulldown on the BSS138. It keeps the MOSFET off if the host GPIO is high-impedance during reset or power-on.
33 kΩ + 10 kΩ resistors, ¼ W, 1% tolerance2Resistive voltage divider for ignition sense: 33 kΩ high-side (to 12 V rail) + 10 kΩ low-side (to GND) → 12 V × 10 / (33 + 10) ≈ 2.79 V at A2, safely within the host's 3.3 V GPIO absolute maximum. The 1% tolerance keeps the threshold consistent. POC-only front end. This bare divider provides no protection against automotive load-dump or starter transients, so see §5 and §11 for production protection guidance.
NEMA 4X weatherproof enclosure, ~150×100×75 mm (e.g. Polycase WC-31 or Hammond 1554N2GYCL)1Rated for outdoor hose-down environments. Mount it inside the equipment housing where nobody can see it from outside.
Skylo-certified cellular/satellite antenna (use the antenna included in the NOTE-NBGLWX kit)1Connects to the MAIN u.FL on Notecard for Skylo. The NOTE-NBGLWX is certified on Skylo's network only with the included antenna. Substituting a different antenna voids the Skylo network certification and may get the device blocked from the network. See the antenna guide for placement requirements.
Passive GNSS antenna, GPS L1 1575 MHz, u.FL connector, outdoor-rated (e.g. Taoglas FXP611 (Cloud) flexible PCB GPS/GLONASS antenna with u.FL lead, or equivalent passive GPS patch)1Connects to the GPS u.FL on Notecard for Skylo. It has to be a passive antenna, because the GPS port doesn't supply bias voltage. Mount it outdoors next to the MAIN antenna with a clear view of the sky.

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

note

Important satellite note. Skylo's constellation sits in geostationary orbit (GEO), so from the northern hemisphere the antenna needs an unobstructed view of the southern sky to lock onto the satellites. A metal equipment cab that blocks the southern horizon will prevent satellite connectivity. Mount the antenna on the roof of the enclosure and orient it accordingly. The Satellite Best Practices guide has more detail on antenna placement.

5. Wiring and Assembly

warning

Safety. The ignition and starter circuits on heavy equipment carry large inrush currents and may involve safety-critical functions. All wiring must be done by qualified personnel following the equipment manufacturer's service manual and applicable electrical codes. The relay wiring described here is only a reference example, so confirm the correct interrupt point for your specific machine (skid steer, generator, light tower, etc.) before installation. Never interrupt circuits that control active running loads (fuel injection, hydraulics) while the engine is running.

The whole assembly has to disappear inside the equipment chassis, behind a panel, under a seat, or in an equipment bay where a thief won't find it. That's why the build is small and the antennas are the only parts that need to see the sky. Notecard for Skylo seats into the Notecarrier CX's M.2 slot. Route the MAIN u.FL lead to the Skylo-certified antenna that came with the kit and the GPS u.FL lead to the separate passive GNSS antenna. Leave both leads slack until you've settled on the final enclosure location, because antenna placement and a clear view of the sky make a big difference to Skylo lock.

During bench validation, the Mojo coulomb counter splices inline on the +VBAT wire to measure current draw phase by phase (the Mojo datasheet shows the wiring). It comes out before the unit ships to the field, unless you add the Qwiic telemetry extension (see §11), in which case it stays in circuit.

warning

12 V systems only. Confirm that the equipment's system voltage is 12 V before you connect the ignition-sense divider or relay coil. On a 24 V machine, the divider would exceed the host GPIO limit and the relay coil would overheat. The warning in §4 explains how to adapt the design to 24 V.

Pin-by-pin wiring:

Power chain (follow this order):

  1. Solar panel (+/−) → charger module solar/panel input terminals.
  2. LiPo battery JST → charger module battery output connector (the dedicated battery terminal on the charger, not its load output). The charger manages charging from the solar panel and keeps the cell from overcharging.
  3. Charger load/output (+) → (during bench validation: Mojo BAT +, then Mojo LOAD + to next step; otherwise directly to next step) → Notecarrier CX battery JST connector (the BATT / +VBAT header). This is the supply rail for both the Notecarrier CX and its embedded STM32 host. Charger load/output (−) and Mojo GND go to common GND.

Before connecting, check that your charger module's output connector pitch and polarity match the Notecarrier CX's JST-PH 2.0 mm battery header. Some charger modules ship with reversed-polarity JST connectors.

Signal wiring:

  • A1 (relay driver GPIO) → BSS138 gate. BSS138 source to GND. A 10 kΩ gate-to-source pulldown resistor (from BSS138 gate to GND) keeps the MOSFET off when the GPIO is high-impedance during reset. BSS138 drain to relay coil pin 85 (coil −). Put an inline 1–2 A automotive fuse on the wire between the 12 V supply tap and relay coil + (pin 86), as close to the 12 V source as practicable. The 1N4007 goes across the coil, cathode to pin 86 (+12 V) and anode to pin 85 (MOSFET drain/coil −), where it clamps the inductive spike when the coil de-energizes. At V_GS = 3.3 V the BSS138 operates inside its R_DS(on)-characterized region (max 3.5 Ω at V_GS = 2.5 V per the Nexperia datasheet), so every wake that asserts the driver pin reliably energizes the relay coil.

  • A2 (ignition sense) → junction node of voltage divider. 33 kΩ high-side resistor from the equipment's 12 V ignition rail to the A2 junction node. 10 kΩ low-side resistor from the A2 junction node to GND. This divides 12 V down to about 2.79 V at the host GPIO (12 V × 10 / (33 + 10) ≈ 2.79 V), safely within the 3.3 V absolute maximum.

    Ignition-sense front end (POC only). The bare resistive divider does nothing to stop the automotive transients that show up on ignition and battery lines in real equipment. Load-dump events on generator-set systems can produce transients of 40–200 V above nominal, and starter motor switching puts fast inductive spikes on the 12 V rail. A production design should add at least a TVS diode (e.g. 15 V unidirectional TVS from the 12 V rail to GND, placed before the divider high-side resistor) and an RC low-pass filter (e.g. 1 kΩ series + 100 nF to GND at the A2 node) to clamp transients and filter noise before they reach the host GPIO. For production deployments, the right long-term answer is an optically isolated or fully automotive-rated sensing front end.

  • Relay switched contacts → NC (normally-closed) contact (pins 87a and 30) wired in series with the starter solenoid enable or ignition/run-start control circuit, which is the low-current signal wire that authorizes cranking. With the coil de-energized, current flows normally, and energizing the coil opens the circuit. Do not wire the relay in the high-current starter motor power cable (the heavy-gauge cable running between the battery positive terminal and the starter motor terminal). That cable carries hundreds of amperes during cranking and isn't the right intercept point for this relay. Check the equipment service manual to find the correct low-current control wire.

  • SDA / SCL → Notecard for Skylo talks over the Notecarrier CX's onboard I²C bus internally, so there's no external wiring to do.

  • 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 (ATTN at position 3, D5 at position 14). The Notecard pulls ATTN low while the host sleeps and raises it when the adaptive wake interval expires, and the rising edge on D5 wakes the host from STOP2. Leave the Notecarrier CX EN pin unconnected. On the CX it enables the shared 3.3 V VIO rail (host, Qwiic, 3V3_OUT, and Notecard I/O), so driving it from ATTN would brown out the whole board instead of putting the host to sleep. Putting a Host to Sleep Between Sensor Readings walks through the full pattern.

  • GND → equipment chassis ground for the ignition sense divider and relay coil return. Keep this common with the Notecarrier CX GND.

Enclosure and antenna mounting. Hide the enclosure somewhere inside the equipment chassis where it isn't obvious, like behind a panel, under a seat, or inside an equipment bay. Route the antenna cable through a weatherproof grommet. For the best Skylo link budget, point the antenna patch at the sky with as much southern-hemisphere clearance as you can get from the installation latitude.

Wiring diagram: A1 → BSS138 → SPDT relay (NC contact in starter solenoid enable circuit); 12 V ignition rail → 33 kΩ/10 kΩ divider → A2; ATTN → D5 jumper so the Notecard can wake the host from STOP2; MAIN u.FL → Skylo-certified antenna; GPS u.FL → passive GNSS patch; solar panel → charger → LiPo → Notecarrier CX +VBAT

6. Notehub Setup

  1. Create a project. Sign up at notehub.io and create a project. Copy the ProductUID and paste it into firmware/construction_equipment_anti_theft/construction_equipment_anti_theft.ino as PRODUCT_UID.

  2. Claim the Notecard. Power up the unit. The Notecard auto-provisions to your project on its first cellular connection. Check that the device shows up in the Notehub device list.

  3. Create a Fleet per job site. Fleets group devices for shared configuration. One fleet per active job site is the natural boundary, since all the equipment on a site shares the same geofence center and after-hours window through fleet-level environment variables. Per-device overrides cover any machine with unusual requirements. Smart Fleets can automate fleet membership based on location data.

  4. Set environment variables. Every variable is optional, and the table shows the firmware defaults. The device pulls variables on each wake with a delta check, so you can retune thresholds without re-flashing the firmware. A cadence value outside its range is clamped to the nearest limit, and a zero or negative value is ignored. When a cadence variable changes, the firmware reissues both hub.set (outbound and inbound session windows) and card.location.mode (GNSS acquisition cadence) so all three Notecard cadences stay in sync with the updated host wake interval.

    VariableDefaultRange / NotesPurpose
    fence_enabled(not set)1 or true to enableRecommended commissioning flag. Set it to 1 to apply fence_lat and fence_lon to the geofence whatever their numeric values. Use it instead of the legacy non-zero-check fallback, because it supports fences centered exactly on latitude 0° or longitude 0°. If fence_enabled is absent, the firmware falls back to checking that fence_lat and fence_lon are both non-zero.
    fence_lat(first GPS fix)Decimal degreesLatitude of the job-site geofence center. Set fence_enabled=1 along with it (see above). If neither fence_enabled nor a non-zero fence_lat is present, the firmware anchors the geofence at the first valid GPS fix after a cold boot.
    fence_lon(first GPS fix)Decimal degreesLongitude of the job-site geofence center. Set fence_enabled=1 to apply it regardless of magnitude, because the legacy fallback silently ignores a fence_lon of exactly 0.0 (the prime meridian).
    fence_radius_m200> 0Geofence radius in meters. 200 m works well for a single job site. Go bigger for large sites or smaller for tighter perimeter security.
    after_hours_start181–23UTC hour when after-hours monitoring starts. 0 counts as "unset" (the firmware keeps the existing value). Adjust for the deployment region's UTC offset.
    after_hours_end60–23UTC hour when after-hours monitoring ends. 0 (midnight) is valid.
    heartbeat_stopped_min601–1440 (clamped)Minutes between host wake cycles and queued heartbeat Notes while the equipment is stationary. It also sets the daytime inbound cadence, since the Notecard polls Notehub once per this interval while parked (see inbound_min). Worst-case geofence-detection latency during business hours equals this value, so lower it for faster detection at the cost of more wake cycles.
    heartbeat_moving_min51–60 (clamped)Minutes between queued heartbeat Notes when the Notecard accelerometer reports motion. It also sets the GNSS acquisition cadence (card.location.mode seconds), so the Notecard gets a fresh position fix at this interval while moving. Heartbeat records pile up in local flash and go out together on the next outbound sync session instead of one at a time at this cadence. Real-time pursuit location updates come from alert.qo Notes (sync:true), not from the heartbeat queue.
    heartbeat_afterhours_min21–30 (clamped)Minutes between host wake cycles during the after-hours window. This sets alert-detection latency, because the host wakes this often to scan for motion and check the command queue. Heartbeats still queue at the stationary/moving rate no matter what this is set to. It does not control the Notecard inbound cadence (see inbound_min).
    inbound_min41–60 (clamped)The short inbound interval used after-hours and when moving. It's how often the Notecard polls Notehub for queued commands and environment-variable updates during active-monitoring windows. When the equipment is stopped during the day, the firmware switches to heartbeat_stopped_min as the inbound interval, so inbound sessions line up with host wakes and the Notecard doesn't open idle cellular connections while the equipment sits parked on the job site. The Notecard drives the inbound poll on its own, and it does not wake the host MCU.
    outbound_min2401–1440 (clamped)Minutes between Notecard outbound batch sessions, which is how often queued heartbeat Notes go to Notehub. It's deliberately longer than heartbeat_stopped_min so several heartbeats collect per outbound session. That cuts total cellular radio time without hurting detection latency. Alert Notes (sync:true) skip this window and go out immediately.
    alert_cooldown_min51–60 (clamped)Minimum minutes between repeated alerts of the same type. Each alert type (geofence_breach, motion_after_hours, cmd_retrieve_failed) has its own cooldown timer, so one alert type can't suppress another. On the Skylo satellite link, consider raising this to 15–30 minutes to protect the 10 KB bundled satellite data budget during a long geofence-breach event.
  5. Issue an immobilize command. From the Notehub UI (or through the Notehub REST API), add a Note with body {"cmd":"immobilize"} to the device's immobilize.qi inbound queue. Command delivery has two sequential latency stages: (1) the Notecard pulls the inbound Note on its next inbound sync session, and (2) the host processes the command the next time it wakes and runs runCycle(). With the default cadence, the worst case from posting the command to seeing the immobilize_armed acknowledgment in Notehub is ~6 minutes during the after-hours window (≤ 4 minutes for the Notecard inbound sync, then ≤ 2 minutes until the next host wake). It's ~9 minutes while moving (4 + 5 minutes) and just under 2 hours while stationary during business hours (up to 60 minutes for the Notecard daytime inbound sync + up to 60 minutes until the next host wake). The daytime inbound interval stretches to match heartbeat_stopped_min (default 60 minutes) to avoid idle cellular sessions. If you need near-real-time command delivery around the clock, reduce heartbeat_stopped_min or add a motion-triggered wake (see §11 Production next steps). To cancel before the relay fires, send {"cmd":"release"}.

    Queue semantics: last command wins. On each wake the firmware drains the entire immobilize.qi queue, calling note.get in a loop until it's empty and applying each command in order. The last command in the queue is the one that takes effect. If you post immobilize and follow it right away with release, the release wins on the same wake cycle. Keep at most one outstanding command in the queue at a time, and don't post a new one until Notehub shows either the immobilize_armed alert (confirming a staged immobilize) or the release_confirmed alert (confirming the immobilize was cleared). Each of those alerts goes out once, on the exact wake cycle that processes its command. This is a staged, edge-triggered POC immobilizer. The relay isn't asserted until the device wakes and sees an OFF→ON ignition edge while the command is staged (one prior wake saw OFF, and the firing wake sees ON). It deliberately won't fire while the engine is already running.

    Important: For a remote immobilizer, this is a security-sensitive control path. Anyone who can post to immobilize.qi can cut ignition on the equipment, so restrict immobilize.qi write access to tightly scoped, audited Notehub API credentials and don't use a project-wide admin token.

  6. Configure routes. Add one route for alert.qo (real-time delivery to a security or on-call platform) and a second for tracker.qo (position history to a fleet-management or asset-tracking store). Because the two Notefiles are already separate at the source, you can send them to different systems at different urgency, and the route itself doesn't need any filter logic.

7. Firmware Design

Every wake runs the same short cycle: sense, evaluate, queue any Notes, and sleep. The application logic and the setup() / loop() entry points live in the main sketch, firmware/construction_equipment_anti_theft/construction_equipment_anti_theft.ino. All Notecard interaction is wrapped up in construction_equipment_anti_theft_helpers.h and construction_equipment_anti_theft_helpers.cpp, and the host-sleep helper is cx_sleep.h.

Dependencies:

  • Arduino Core for STM32 (stm32duino/Arduino_Core_STM32). The Notecarrier CX's host uses the Cygnet board definition, which you'll find in the Arduino Boards Manager (see the build settings below).
  • Blues Wireless Notecard (note-arduino library). Install it from the Arduino Library Manager or with arduino-cli lib install "Blues Wireless Notecard".
  • STM32duino Low Power (and its dependency STM32duino RTC), both from the Library Manager. It provides the STOP2 entry and ATTN-pin wake that cx_sleep.h uses.

Build settings: The Notecarrier CX's host is the same STM32L433 as the Blues Cygnet, so select Blues boards → Cygnet (pnum=CYGNET) and set USB support (if available) to None (usb=none). If the USB CDC stack is enabled and no USB host is attached (battery power, or the Notecarrier CX USB-C switch in NC), the STM32's USB wakeup interrupt fires continuously and the host never stays in STOP2. Debug output (when DEBUG_SERIAL is set to 1 in the sketch) 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).

Modules

ResponsibilityWhere
Notecard configuration: hub.set, card.transport (wifi-cell-ntn, which turns on satellite fallback), GPS mode, motion mode, templates (idempotent, runs every wake)ensureConfigured()
Compact Note templates for satellitedefineTemplates()
Env-variable delta pullfetchEnvOverrides()
Match hub inbound/outbound and GNSS cadences to the wake-state contextapplyHubCadence()
Drain inbound command queue (last command wins)checkAndHandleCommand()
Ignition state, motion state, GPS fix, battery voltagegetIgnitionState(), getIsMoving(), getLocation(), getBatteryVoltage()
Geofence evaluation (Haversine)haversineDistanceM()
After-hours window evaluation (UTC)isAfterHours()
Heartbeat and alert Note emissionsendHeartbeat(), sendAlert()
Relay assertion / releaseassertRelay(), releaseRelay()
Host sleep (STOP2 until the Notecard raises ATTN)cx_sleep.h: cxSleepBegin(), cxSleepUntilAttn()
Application state (held in RAM across sleep cycles)AppState
Geofence persistence across power losssaveFenceToFlash(), loadFenceFromFlash() (fence.db)

Sensor reading strategy

Ignition sense. With the ignition key ON, a voltage divider on the equipment's 12 V ignition rail puts ~2.79 V on the host's A2 pin, inside the 3.3 V safe limit. The high-side resistor (33 kΩ, to the 12 V rail) and low-side resistor (10 kΩ, to GND) set the divider ratio: 12 V × 10 / (33 + 10) ≈ 2.79 V. On each wake the host reads A2 with analogRead() at 12-bit resolution and averages 4 samples to filter out automotive line noise and battery-sag transients during engine crank. Hysteresis thresholds govern state changes. The averaged reading has to rise above ~2.50 V (3103 counts on a 12-bit / 3.3 V ADC) to declare ignition ON, and fall below ~2.00 V (2482 counts) to declare ignition OFF. Inside the ~0.5 V hysteresis band the firmware holds the previous state. That prevents chatter at the logic boundary and spans the typical battery-sag floor during cold cranks (~10.5 V → ~2.44 V at the divider).

Motion. The built-in accelerometer on Notecard for Skylo runs continuously in low-power mode. card.motion.mode configures it for 25Hz, ±4G sensitivity with a 60-second bucket. The firmware calls card.motion on each wake, and when the bucket holds at least 3 events, the Notecard reports "mode":"moving". That catches equipment being driven away (sustained multi-axis motion) as well as equipment being loaded onto a trailer (tilt, vibration).

GPS. card.location.mode is set to periodic with a motion threshold of 4 events, so the Notecard only spins up the GNSS module once the accelerometer confirms the unit is actually moving. That spares stationary wakes the multi-second GNSS warm-up penalty. The firmware calls card.location to read the latest fix, and because the Notecard caches the last valid location, the call returns quickly even when GNSS isn't active.

note

Cached-fix latency. Geofence and alert decisions use the most recently cached GNSS fix, which may not reflect where the device is right now. The GNSS acquisition cadence bounds the fix age (card.location.mode seconds, default 5 minutes, tunable with heartbeat_moving_min). During the after-hours window the host wakes every 2 minutes but GNSS only updates every 5 minutes, so two or three wakes in a row may check the geofence against the same cached fix. The firmware captures the time field from card.location (the epoch of the cached fix) and computes fix_age_s = now − fix_time, but only when card.time returns a valid non-zero epoch. When card.time isn't available, for example on first boot before the Notecard has completed a Notehub session, fix_age_s goes out as -1 (a sentinel meaning "age unknown") instead of 0. That lets downstream consumers tell a truly fresh fix apart from one whose age just can't be determined. A value of 0 or greater is a real elapsed-seconds count. Every outbound Note includes fix_age_s, both the tracker.qo heartbeats and the alert.qo events.

Battery voltage. card.voltage returns the LiPo rail voltage directly from the Notecard's internal ADC. Every Note includes it so you can trend battery state of charge.

Event payload design

Both Notefiles use compact Note templates with a port number. Notecard for Skylo requires compact templates. They store Notes as fixed-length binary records instead of free-form JSON, which shrinks each Note's wire size by 3–5×.

The compact record sizes come straight from the template field types declared in defineTemplates():

  • tracker.qo: 4 floats (lat, lon, bat_v, fix_age_s) × 4 bytes + 5 int16 fields (loc_valid, ignition, moving, immobilized, fence_ok) × 2 bytes = 26 bytes per heartbeat record.
  • alert.qo: 4 floats (lat, lon, bat_v, fix_age_s) × 4 bytes + 3 int16 fields (loc_valid, ignition, immobilized) × 2 bytes + 1 string field (alert, 24-char sample) at 25 bytes (1-byte length prefix + 24 data bytes) = 47 bytes per alert record.

With the 10 KB of bundled Skylo data (10,240 bytes), payload-only capacity is roughly 390 heartbeat-sized Notes (10,240 ÷ 26 bytes) or 215 alert-sized Notes (10,240 ÷ 47 bytes) in total. Protocol and session framing overhead push the real delivered counts below those payload-only maxima, so validate against Notehub usage metrics and field measurements instead of relying on payload-only arithmetic. §11 has practical budget guidance.

Example heartbeat (tracker.qo):

{
  "file": "tracker.qo",
  "body": {
    "lat":         37.3387,
    "lon":       -121.8853,
    "loc_valid":   1,
    "ignition":    0,
    "moving":      0,
    "immobilized": 0,
    "bat_v":       3.92,
    "fence_ok":    1,
    "fix_age_s":   47.0
  }
}

fix_age_s is the age of the cached GNSS fix, in seconds, at the moment the Note was queued. -1.0 means card.time wasn't available when the Note was queued. In that case the true fix age can't be determined, so don't treat it as a freshness indicator. A value of 0.0 or greater is a real elapsed-seconds count. Anything well below heartbeat_moving_min × 60 (default 300 seconds) indicates a fresh fix. A value near or past that threshold means the geofence and alert decisions on this wake used a cached position from the previous GNSS acquisition cycle.

Example alert (alert.qo), sent with sync:true:

{
  "file": "alert.qo",
  "body": {
    "alert":       "geofence_breach",
    "lat":         37.3521,
    "lon":       -121.9204,
    "loc_valid":   1,
    "ignition":    1,
    "immobilized": 0,
    "bat_v":       3.87,
    "fix_age_s":   112.0
  },
  "sync": true
}

Alert types:

alert valueMeaning
geofence_breachEquipment GPS fix is outside the configured fence radius
motion_after_hoursAccelerometer detects motion during the after-hours window while ignition is confirmed OFF
ignition_on_immobilizedImmobilize was staged, and the relay asserted on an OFF→ON ignition edge (not on a level read of ignition-ON)
immobilize_armedAcknowledgment Note confirming the immobilize command was received and staged
release_confirmedAcknowledgment Note confirming a release command cleared the staged or active immobilize, with the relay de-energized
cmd_retrieve_failedDiagnostic: note.get on immobilize.qi ran out of retries or returned an error other than the normal empty-queue {note-noexist}, which means the command path is unhealthy. It's rate-limited by alert_cooldown_min (default 5 minutes) so a persistent I²C or Notecard fault doesn't generate repeated sync:true traffic. If this alert keeps firing, look at the I²C wiring and Notecard firmware.

Low-power strategy

Both the STM32 host and the Notecard sleep between sensing cycles. At the end of each wake, runCycle() calls cxSleepUntilAttn() (in cx_sleep.h). That issues card.attn with mode:sleep and the adaptive duration, waits for the Notecard to pull ATTN low, and puts the host into STOP2, where it draws a few microamps and keeps its RAM and its GPIO state intact. Between cellular sync windows the Notecard drops into its own low-power idle state (~8–18 µA @ 5V). After sleep_sec seconds the ATTN pin goes HIGH, and the rising edge on D5 wakes the host. Execution picks up on the next line with g_state exactly as it was, and setup() only runs once per power-up. If the sleep request fails or ATTN never goes low (a missing jumper), the firmware waits out the same adaptive interval awake and tries again on the next cycle, so the sensing cadence holds either way.

The sleep duration depends on context: 2 minutes after hours (fast theft detection), 5 minutes while moving (active tracking), and 1 hour at rest during business hours (background monitoring, which also sets the worst-case geofence-detection latency while parked). The after-hours wake interval is deliberately shorter than the heartbeat interval. The device wakes every 2 minutes to check for alerts and inbound commands, but heartbeat Notes queue at the stationary or moving rate at any time of day. That keeps overnight Note volume in line with daytime operation and still catches a theft within the scan window.

Sampling/queuing cadence and transmission cadence are always distinct. The host queues heartbeat Notes into the Notecard's local flash, and they go to Notehub on the next outbound sync session, which fires at the hub.set outbound interval (set by outbound_min, default 240 minutes). At the 5-minute moving sample rate, the Notecard collects a heartbeat every 5 minutes while in motion, but those records still go out as a batch on the next outbound session unless an alert happens to force a sync:true flush first. Only alert.qo Notes (with sync:true) skip the outbound window and trigger an immediate uplink sync.

The Notecard also manages its own inbound sync sessions. These are cellular connections the Notecard opens on its own schedule to pull queued commands and updated environment variables from Notehub. The inbound cadence follows context. When the equipment is parked during the day, the interval matches heartbeat_stopped_min (default 60 minutes, set through hub.set inbound). After hours or while moving, it shortens to inbound_min (default 4 minutes). The Notecard drives these sessions, and they happen whether or not the host is awake. The host's 2-minute after-hours wake cycle and the Notecard's inbound cellular cadence are independent and tuned with separate parameters (heartbeat_afterhours_min and inbound_min, respectively). That's how daytime inbound sessions can match the host wake cadence instead of running dozens of times an hour against a parked asset. Account for both cadences when you size the energy budget (see §9).

Env-var cadence retuning scope. When an operator changes any cadence variable (heartbeat_moving_min, heartbeat_stopped_min, heartbeat_afterhours_min, inbound_min, or outbound_min) in Notehub, the firmware catches the change with the env.get delta check and calls applyHubCadence(). That reissues hub.set (outbound and inbound windows) and card.location.mode (GNSS acquisition seconds, set to heartbeat_moving_s). The firmware also calls applyHubCadence() right away whenever the motion or after-hours context changes, so the Notecard's inbound window switches between the short active-monitoring interval (inbound_min) and the long parked interval (heartbeat_stopped_min) without waiting for a future env-var change. The outbound window uses outbound_s (set by outbound_min, default 240 minutes) and is decoupled from the host wake interval. Env-var changes don't affect card.motion.mode (accelerometer sensitivity and bucket size). Those are set at power-up only, and changing them takes a firmware update.

The Notecard gates the GPS module on motion, so it only turns on after sustained accelerometer activity. The GNSS radio stays silent through the many hours a day the equipment sits parked.

Retry and error handling

  • The firmware checks the card.attn sleep request in cxSleepUntilAttn(), since the host stays awake long enough to see the response.
  • All requestAndResponse calls check for a NULL response before touching the returned JSON. Calls to fetchEnvOverrides, checkAndHandleCommand, and getLocation also check for a non-null "err" field, and so do calls to getIsMoving, getBatteryVoltage, and getEpochTime. A sensor call that fails returns a safe default (zero for GPS coordinates, false for motion/ignition) instead of pushing bad data into alerts or Notes.
  • note.get on immobilize.qi returns an err tagged {note-noexist} when no Note is queued. The firmware treats that as the normal end of the queue and continues the wake cycle. Any other error raises cmd_retrieve_failed.
  • env.get with the time delta parameter still makes the I²C request on every wake. When no environment variables have changed since the last pull, though, the Notecard returns an err response instead of the full env-var payload. The firmware detects that and skips the overhead of re-parsing and re-applying unchanged values.
  • When env.get does return updated values, the firmware immediately reissues hub.set through applyHubCadence() so the Notecard's outbound and inbound session windows pick up any changed heartbeat intervals. If that reissue is skipped (e.g., the Notecard isn't ready), the cadence stays at the last value that was applied successfully.
  • Alert de-duplication keeps a separate cooldown timestamp for each type (last_geofence_alert_s, last_motion_alert_s, and last_cmd_failed_alert_s), so a geofence-breach cooldown can't suppress a concurrent after-hours motion alert, or the other way around.

Key code snippet 1: compact Note template definition

Notecard for Skylo needs the format:"compact" and port arguments. Without them, Notes headed for the satellite path may be dropped. defineTemplates() registers tracker.qo (TRACKER_NOTEFILE) on port 50 (TRACKER_PORT) as shown below, and alert.qo the same way on port 51 (ALERT_PORT). Each request is retried up to three times. The numeric type hints (14.1 = 4-byte float, 12 = 2-byte signed int) define the binary record layout: 4 floats × 4 bytes + 5 int16 fields × 2 bytes = 26 bytes per heartbeat. fence_ok is 1 in-fence, 0 on a breach, and -1 when unknown.

for (int attempt = 0; attempt < kMaxAttempts && !ok; attempt++) {
    J *req = nc.newRequest("note.template");
    JAddStringToObject(req, "file",   TRACKER_NOTEFILE);
    JAddStringToObject(req, "format", "compact");
    JAddNumberToObject(req, "port",   TRACKER_PORT);
    J *body = JAddObjectToObject(req, "body");
    JAddNumberToObject(body, "lat",         14.1);
    JAddNumberToObject(body, "lon",         14.1);
    // ...
    JAddNumberToObject(body, "loc_valid",   12);
    JAddNumberToObject(body, "ignition",    12);
    JAddNumberToObject(body, "moving",      12);
    JAddNumberToObject(body, "immobilized", 12);
    JAddNumberToObject(body, "bat_v",       14.1);
    JAddNumberToObject(body, "fence_ok",    12);
    // fix_age_s: seconds since the GNSS fix was last acquired (>= 0), or
    // -1 when card.time was unavailable and the age cannot be determined.
    // Downstream consumers must treat -1 as "age unknown".
    JAddNumberToObject(body, "fix_age_s",   14.1);
    ok = sendRequestChecked(nc, req);
    // ...
}

Key code snippet 2: immediate-sync alert

sync:true wakes the radio and flushes this Note immediately, skipping the normal outbound window. Whether a geofence breach alert reaches the operator in seconds or in hours can decide whether the equipment gets recovered.

sendAlert() builds the Note below for every alert type (alert_type is "geofence_breach", "motion_after_hours", and so on). It checks the response for an err field and retries up to three times, so a transient I²C error doesn't drop a security alert.

const int kMaxRetries = 3;
for (int attempt = 0; attempt < kMaxRetries; attempt++) {
    J *req  = nc.newRequest("note.add");
    JAddStringToObject(req, "file", ALERT_NOTEFILE);
    JAddBoolToObject(req, "sync", true);   // wake radio, transmit now
    J *body = JAddObjectToObject(req, "body");
    JAddStringToObject(body, "alert",       alert_type);
    JAddNumberToObject(body, "lat",         lat);
    JAddNumberToObject(body, "lon",         lon);
    // ...
    J *rsp = nc.requestAndResponse(req);
    // ...
}

Key code snippet 3: relay assertion on key-on edge

The immobilizer doesn't cut power while the engine is running. It stages the relay and waits for the next OFF→ON ignition transition, which is an edge and not a level check. The firmware keeps the previous wake's ignition state in g_state.last_ignition_on so it can detect the edge across sleep cycles. If the equipment was already running when the operator issued the immobilize command, the firmware deliberately holds off until the thief turns the key OFF and back ON. That keeps it from cutting the starter circuit while the engine is actively running and avoids the safety hazards called out in §5.

bool ignition_edge = (!g_state.last_ignition_on && ignition_on);
if (g_state.immobilize_pending && ignition_edge && !g_state.immobilized) {
    assertRelay();
    g_state.immobilized        = true;
    g_state.immobilize_pending = false;
    LOGLN("[APP] Relay asserted on key-on edge — ignition blocked.");
    if (!sendAlert(notecard, g_state,
                   "ignition_on_immobilized", cur_lat, cur_lon, got_fix,
                   true, true, fix_age_s)) {
        LOGLN("[APP] ERROR: immobilize alert failed after all retries.");
    }
}

Key code snippet 4: adaptive sleep until the Notecard raises ATTN

g_state stays in RAM through the sleep, so the next wake has full context no matter what triggered it. The duration adapts to the current risk level. If the sleep request fails, the firmware waits out the same interval awake so the cadence holds.

uint32_t sleep_sec = afterhrs
    ? (uint32_t)g_state.heartbeat_afterhours_s
    : moving ? (uint32_t)g_state.heartbeat_moving_s
             : (uint32_t)g_state.heartbeat_stopped_s;
// ...
// Sleep in STOP2 until the Notecard raises ATTN sleep_sec seconds from now.
// cxSleepUntilAttn() sends card.attn "sleep" (checked, since the host stays
// alive to see the response), waits for ATTN to go low, and enters STOP2.
// Execution resumes on the next line; g_state stays in RAM.
// ...
if (!cxSleepUntilAttn(notecard, sleep_sec, NULL, log)) {
    // The Notecard didn't take the sleep request, or ATTN never went low
    // (check the ATTN -> D5 jumper).  Wait out the same adaptive interval
    // awake so the sensing cadence — and the after-hours scan rate — is
    // preserved, then return to loop(), which calls runCycle() again with
    // g_state intact (staged immobilize, alert cooldowns, heartbeat timing,
    // and fence state are all still in RAM).
    // ...
    delay(sleep_sec * 1000UL);
}

8. Data Flow

Data flow: ignition/motion/GPS/battery → host edge rules → alert.qo (sync:true, immediate) and tracker.qo (queued, batch outbound) → Notehub → routes; immobilize.qi inbound from Notehub to host → relay

Collected every wake cycle. Ignition state (12V voltage divider), motion status (Notecard accelerometer, read with card.motion), GPS fix (Notecard GNSS, read with card.location), battery voltage (card.voltage), and UTC epoch (card.time). The design uses no I²C sensors outside the Notecard, because all of the sensing is built into the Notecard for Skylo module.

Transmitted:

  • tracker.qo: heartbeat Notes are queued at the moving/stopped sample rate (every 5 minutes while moving, every 60 minutes while stationary), then transmitted in batches on the hub.set outbound schedule (default 240 minutes, set by outbound_min). Moving heartbeats collect in the Notecard's local flash at the 5-minute sample rate, but they don't reach Notehub one at a time on that cadence. They're flushed as a batch when the outbound window fires, or earlier if a sync:true alert happens to force a session. So real-time pursuit tracking relies on repeated alert syncs, not on the heartbeat delivery cadence. In normal stationary operation, a properly secured job site generates 24 heartbeat Notes queued per 24 hours (one per hour), 6 outbound sessions per 24 hours (one every 4 hours, each carrying ~4 heartbeats), zero alert.qo Notes, and fence_ok:1 in every heartbeat. Inbound sessions depend on context: roughly 12 sessions during the 12-hour business day while parked (every 60 minutes), and up to roughly 180 sessions during the 12-hour after-hours window (every 4 minutes). All of them count toward the daily radio-session total and energy budget.
  • alert.qo: sent only when a rule trips, with sync:true to bypass the outbound queue. Each alert type (geofence_breach, motion_after_hours) has its own 5-minute cooldown, so slow geofence oscillations or intermittent wind-buffeted vibration can't flood the alert channel, and one alert type can't suppress the other.

Routed. Both Notefiles reach Notehub over cellular (LTE-M / NB-IoT) or, when cellular coverage fails, over Skylo satellite NTN (non-terrestrial network). Routing from Notehub to the operator's downstream system of choice depends on the project (see the routing docs).

Alert triggers:

  • geofence_breach: the cached GPS fix is more than fence_radius_m from the stored geofence center. The check uses the most recently cached fix from card.location, and fix_age_s in the alert body shows how stale that fix was when the alert fired (-1 if card.time was unavailable).
  • motion_after_hours: the Notecard accelerometer reports "mode":"moving" during the after-hours window AND ignition is confirmed OFF. Motion with the ignition off is the telltale sign of equipment being towed or loaded instead of driven.
  • ignition_on_immobilized: the host sees an OFF→ON ignition edge while immobilize_pending is set and asserts the relay on that same wake cycle. This is a staged, edge-triggered immobilizer. The relay does not fire while the engine is already running, only on the next key cycle the host observes (the previous wake's ignition state must have been OFF).
  • immobilize_armed: the operator's command was received and staged, and the relay will fire on the next OFF→ON ignition edge the host observes.
  • cmd_retrieve_failed: note.get on immobilize.qi ran out of retries or returned an unexpected error on this wake cycle, so the command path is unhealthy. It's rate-limited by alert_cooldown_min (default 5 minutes) so a persistent fault can't flood the operator's inbox or the satellite budget.

Inbound commands. The operator sends {"cmd":"immobilize"} or {"cmd":"release"} to immobilize.qi from the Notehub UI or REST API. End-to-end command latency has two sequential parts. First, the Notecard has to complete an inbound sync session to pull the queued Note from Notehub. That interval depends on context: inbound_min (default 4 minutes) after hours or while moving, or heartbeat_stopped_min (default 60 minutes) when parked during the day. Second, the host has to wake and run runCycle(), where checkAndHandleCommand() drains the entire immobilize.qi queue by calling note.get in a loop until it's empty, applying each command in order (last command wins). The host wake interval also depends on context: ≤ 2 minutes after hours, ≤ 5 minutes while moving, and ≤ 60 minutes by default when stationary during business hours.

Under default settings, the combined worst-case end-to-end latency is up to ~6 minutes after hours (≤ 4 minutes inbound sync + ≤ 2 minutes wake interval) and up to ~9 minutes while moving (4 + 5 minutes). The worst window is a stationary unit during business hours, at just under 2 hours from posting the command to seeing the immobilize_armed acknowledgment in Notehub (up to 60 minutes for the daytime inbound sync + up to 60 minutes of wake interval). If you need near-real-time command delivery around the clock, reduce heartbeat_stopped_min, or see §11 Production next steps for the interrupt-driven ignition sense upgrade. Keep in mind that inbound delivery needs cellular or WiFi connectivity. immobilize.qi has no compact template, and the Notecard only syncs templated Notefiles over satellite, so a command staged while the unit is out of cellular coverage gets delivered once cellular is restored.

9. Validation and Testing

Expected steady-state cadence. A correctly deployed tracker on a secure job site queues tracker.qo heartbeat Notes at the stationary cadence (every 60 minutes by default, day and night), sends zero alert.qo Notes, and reports fence_ok:1 in every heartbeat. Notes collect in Notecard flash and go out in batches every 4 hours by default, so expect 24 heartbeats queued per 24-hour period (6 outbound sessions, each carrying ~4 Notes). After hours the device wakes every 2 minutes to scan for motion and check inbound commands, but it only emits a heartbeat when the 60-minute interval is up. Any single alert.qo event is worth investigating.

Geofence first-light test. With the unit powered and showing as connected in Notehub, set fence_lat and fence_lon to the device's current coordinates (you can see them in Notehub's device view), and set fence_radius_m to something small (e.g. 20). Then wait one inbound sync cycle (default 4 minutes after hours, 60 minutes while parked during the day) for the env-var update to reach the device.

tip

Important: Motion does not wake the host MCU. The host only polls card.motion when it wakes on its normal timer, and during business hours a stationary unit wakes every 60 minutes (default heartbeat_stopped_min). Physically moving the device won't get a response until the next scheduled wake. To make the test practical, do one of these before you move the device:

  • Run the test during the after-hours window (default 6 PM–6 AM UTC), when the host wakes every 2 minutes.
  • Temporarily reduce heartbeat_stopped_min to 2 in Notehub env vars and wait for the update to take effect.
  • With DEBUG_SERIAL enabled, connect an ST-LINK to the Notecarrier CX debug jack so you can watch for an active runCycle() wake, and move the device right after one.

Once the device has woken with the updated geofence and you've moved it ~25 m outside the radius, a geofence_breach alert should show up in the Notehub event log after the next qualifying wake cycle that has a sufficiently fresh GNSS fix. Geofence decisions use the most recently cached fix from card.location (bounded by the card.location.mode periodic interval, default 5 minutes), so one or two wake cycles may pass before the Notecard gets a new fix that puts the device outside the radius and trips the alert. The alert fires in the same runCycle() that evaluates the breach, so nothing defers it. Its timing depends on how fresh the GNSS fix is as well as on host-wake latency.

Immobilizer test. From the Notehub UI, post {"cmd":"immobilize"} to immobilize.qi. Two steps have to finish before the host acts on it: (1) the Notecard retrieves the Note on its next inbound sync, and (2) the host wakes and calls note.get in checkAndHandleCommand(). To test this in a reasonable amount of time, either reduce heartbeat_stopped_min to 2 (matching the after-hours cadence) or bench-test during the after-hours window, when the host wakes every 2 minutes. Watch the Notehub event log for immobilize_armed. The host queues that Note after it processes the command, so seeing it confirms the host wake cycle finished. The relay fires on an OFF→ON ignition edge, not a level check, which means at least one wake has to record ignition OFF before the firing wake records ignition ON. To simulate a key-on edge, hold A2 at 0 V (ignition divider output low) for one full wake cycle, then pull A2 to 3.3 V (or briefly short the voltage divider output high) before the next wake. On the wake that sees the OFF→ON transition, ignition_on_immobilized should appear in alert.qo and the relay driver pin (A1) should go HIGH.

Power validation with Mojo. Treat the table below as a starting point for sizing and diagnosis, not a substitute for a bench soak. There's one important caveat on the published Notecard figures. Blues publishes Notecard current at 5 V on the VUSB input, and at the LiPo rail (~3.7–4.2 V) the same power dissipation shows up as proportionally higher current. Blues doesn't publish system-level totals (host + charger quiescent + relay driver), so measure them on your specific build with the Mojo.

The published Notecard for Skylo figures come from the NOTE-NBGLWX low-power design guide and the NOTE-NBGLWX datasheet. The system-level totals add the STM32 host, the solar charger's quiescent draw, and the relay driver overhead to the Notecard baseline. Blues doesn't publish those, so you have to characterize them on your own build. Put the Mojo inline on the +VBAT rail and measure each phase instead of leaning on generic estimates.

PhaseNotecard-only (published, @ 5 V)Whole-device guidance (measure with Mojo)
Deep sleep (host in STOP2, Notecard idle)~8–18 µA (published)Notecard contributes ~8–18 µA, the STM32L433 host in STOP2 adds ~1–2 µA, and the solar charger's quiescent draw adds a few µA more, so expect a floor around ~10–20 µA. Measure the total on your build.
Host active, I²C transactions only (no radio)N/AHost active current dominates. Measure it with the Mojo on your build.
Notecard cellular session (LTE-M)~250 mA while the modem is active (published). Millisecond spikes up to nearly 2 A only apply where GSM is in use.The Notecard dominates, and the host and relay driver add a little on top. Measure with the Mojo for system totals.
Notecard satellite session (Skylo NTN)Not separately publishedMeasure with the Mojo outdoors once you have confirmed satellite lock. Session current and duration depend on antenna placement and sky-view geometry, so getting accurate numbers takes field testing.

Sizing the energy budget. Two independent cadences drive the radio, and the power budget has to account for both:

  1. Inbound sync sessions follow context. While the equipment is parked during the day, the Notecard inbound interval matches heartbeat_stopped_min (default 60 minutes), which works out to roughly 12 inbound sessions over the 12-hour business day. During the after-hours window the interval drops to inbound_min (default 4 minutes), for up to roughly 180 inbound sessions per 12-hour after-hours period. In practice the Notecard combines many of these with outbound sessions, so the real radio-on count is lower. Under default settings the combined total tops out around 200 sessions/day (12 + 180 inbound plus 6 outbound). That's far fewer than a constant 4-minute poll, and it drops further if the after-hours window is narrower.

  2. Outbound sync sessions carry queued heartbeat Notes to Notehub on the hub.set outbound schedule (default 240 minutes, set by outbound_min), roughly 6 outbound sessions per 24 hours. Each one usually carries the ~4 heartbeat Notes queued during its 4-hour window. Alert syncs (sync:true) add short extra bursts when a rule trips.

The host wake cycles (every 2 minutes after hours, every 5 minutes while moving, every 60 minutes at rest by default) are host-only active phases. Each one is a few seconds of I²C transactions, and the radio stays out of it unless an alert fires during that wake.

Measure the real phase currents on your build with a Mojo bench soak, then estimate the daily mAh draw for your deployment's cadence and size the LiPo and solar panel to match. Use the Mojo bench soak to validate that estimate too. Splice it inline on the LiPo-to-Notecarrier-CX +VBAT wire and measure a 24-hour soak. A continuous draw of >50 mA means the host isn't entering STOP2, so check the ATTN→D5 jumper and confirm the firmware was built with the usb=none board option. If the trace looks right but alerts don't go out, check the antenna placement and the hub.set outbound configuration.

To validate a satellite session, the antenna has to be outdoors with a clear view of the sky toward the equator, and the first satellite acquisition can take several minutes. The Satellite Best Practices guide covers troubleshooting for antenna placement and acquisition timing.

10. Troubleshooting

Device not appearing in Notehub after power-on.

  • Confirm USB power is stable (green LED on the Notecarrier CX). If there's no LED, the USB connection may be faulty.
  • Check that the ProductUID is pasted correctly into the firmware sketch (top of construction_equipment_anti_theft.ino). An empty or mismatched ProductUID prevents auto-provisioning.
  • Make sure the antenna cables are fully seated in their u.FL connectors. A loose MAIN antenna means no cellular lock and no Notehub connection.
  • If you're on Skylo satellite only (no cellular), the first satellite acquisition can take 2–5 minutes even with a clear view of the sky. Point the antenna at the southern sky and give it 5 minutes before you call it a failure.
  • If DEBUG_SERIAL is enabled in the firmware, open the ST-LINK virtual COM port (Notecarrier CX debug jack, 115200 baud) to watch live I²C activity and Notecard responses.

Heartbeats appear but fix_age_s is always -1 or very large.

  • fix_age_s = -1 means card.time was unavailable when the Note was queued. That's normal on a cold boot, before the Notecard has synced with Notehub. After one successful outbound sync, card.time should populate and fix_age_s will show real elapsed seconds.
  • If fix_age_s stays at -1 after several outbound syncs, check that cellular or satellite sessions are completing (look for successful sync activity in Notehub Events). A device that can't reach the network never completes a session, so it never gets epoch time.
  • If fix_age_s is consistently >300 seconds (approaching heartbeat_moving_min × 60), the Notecard's GNSS module isn't getting fresh fixes. Check the antenna placement and sky view. GPS acquisition usually takes 20–60 seconds in open sky, and metal roofs or urban canyons can stretch that out a lot.

Geofence alerts never fire even though device moves outside the configured radius.

  • Confirm that fence_enabled is set to 1 and that fence_lat/fence_lon are filled in on Notehub (Fleet Settings → Environment). Without fence_enabled=1, the firmware falls back to checking that both coordinates are non-zero, which fails at the prime meridian (0°,0°).
  • Make sure the device has woken since you set the env vars. The inbound sync interval is 4 minutes (default inbound_min after hours) or 60 minutes (default heartbeat_stopped_min during business hours). Watch the Events log for a heartbeat after the sync should have completed.
  • If a heartbeat shows up but there's still no alert after you've moved outside the radius, check the heartbeat's fix_age_s. Over 300 seconds means the GNSS fix is stale and may not reflect your current position. Move the device and wait for the next wake so GNSS can re-acquire, which can take 5 minutes if the device was stationary and GNSS wasn't active.
  • During business hours the device only wakes every 60 minutes (default). For a quick geofence test, reduce heartbeat_stopped_min to 2 in Notehub and wait for it to take effect. While parked during the day, the inbound sync that delivers it can take up to 60 minutes, and the host only reads it on its next wake (up to 60 minutes more). Then move the device and wait for the next 2-minute wake cycle.

Immobilizer command appears in Notehub but relay never fires.

  • Confirm that immobilize_armed shows up in the Events log within the expected latency (worst case: 60 minutes inbound sync + 60 minutes host wake = 2 hours during business hours). If immobilize_armed never appears, the device never received the command.
  • If immobilize_armed does appear, the command was staged and the relay just didn't fire. The relay fires on an OFF→ON ignition edge (not a level check), so make sure you simulated the edge correctly. The A2 GPIO has to sit at GND (0V, ignition OFF) for at least one full wake cycle, then go to 3.3V (ignition ON) before the next wake. The relay only asserts on the wake that observes the transition.
  • Check the relay driver wiring (A1 GPIO → BSS138 gate). If the A1 wiring is loose or shorted to GND, the relay can't energize. Also confirm the 10 kΩ pulldown is in place (gate to GND) to keep the MOSFET off when the GPIO is high-impedance.
  • If A1 goes HIGH but the relay doesn't click, check that the 1N4007 diode is oriented correctly (cathode to +12V, anode to MOSFET drain). A reversed diode conducts as soon as the MOSFET turns on and shorts out the coil, which can also blow the coil fuse or damage the MOSFET.

High power draw (Mojo shows >50 mA sustained).

  • That means the host isn't entering STOP2. Check the ATTN→D5 jumper (§5). Without it, cxSleepUntilAttn() never sees ATTN go low and waits out each interval awake. Also confirm the firmware was built with USB support set to None (usb=none). With the USB CDC stack enabled and no USB host attached, the host leaves STOP2 within a millisecond.
  • Make sure DEBUG_SERIAL is commented out (not #define DEBUG_SERIAL 1) in field builds, because the debug UART adds to the active-wake current.
  • Power the unit from the LiPo + solar charger instead of bench USB to see true sleep behavior on the Mojo.
  • Check the Notecard firmware version. card.version should show a recent Skylo-certified build, since outdated firmware may have high quiescent current. Update it over the air from Notehub (OTA DFU with Notehub).

Satellite acquisition fails or times out.

  • Skylo satellites sit in geostationary orbit (GEO) over the equator, so the antenna needs a clear view of the southern sky. From the northern hemisphere the satellite is low on the southern horizon, and objects above 15° south-of-horizon elevation may block the link.
  • The first satellite acquisition can take 2–5 minutes, and antenna orientation and a clear sky are critical. Satellite Best Practices has detailed placement guidance.
  • When Skylo is the fallback and cellular is available, the device prefers cellular and never triggers satellite. To test satellite, disconnect cellular or take the device somewhere with no cellular coverage but a clear view of the sky (e.g., a remote rural location).
  • Make sure the antenna on Notecard for Skylo (NOTE-NBGLWX) is the Skylo-certified antenna that came with the kit. A substitute may not lock onto the Skylo network. The datasheet lists the antenna part number.

11. Limitations and Next Steps

This POC shows the control path, the data path, and the satellite fallback working, but it deliberately stops short of being a production security device. A working immobilizer for a deployed fleet needs a latching relay that survives sleep gaps, a transient-protected ignition front end that can handle automotive load dump, and an interrupt-driven wake that catches a key-on edge within the next 100 ms instead of the next two minutes. The items below cover each of those, first as the reference design's scope limits and then as the engineering work that turns it into a production product.

Simplified for the POC

Each item below is a spot where the POC deliberately falls short of a production security device, along with the engineering work that would close the gap.

Staged, polled, edge-triggered immobilizer, not a continuously-held cut. The immobilizer in this POC is a next-wake / next-key-on-edge design. The host sleeps in STOP2 instead of powering off, so its GPIO state is retained and the relay coil stays energized through the sleep window once immobilized = true. The catch is that the standard relay then draws continuous coil current (tens to ~150 mA at 12 V, depending on the relay) from the equipment battery for as long as the immobilize is active. A wiring interruption or host reset still releases the NC contact and restores the ignition circuit, too. The relay also only fires on an OFF→ON ignition edge, so the previous wake must have observed ignition OFF and the current wake must observe ignition ON. If the equipment is already running when the immobilize command is staged, the firmware deliberately holds off until the next key cycle, which avoids cutting the starter circuit while the engine is actively running (per the safety guidance in §5). And if the thief cycles the key all the way between two host wakes (OFF→ON→OFF inside one sleep window), the firmware misses the firing edge entirely until the next OFF→ON transition it observes. Treat this as a proof-of-concept staging mechanism. Production deployments need a latching (bistable) relay that holds its contact state with no continuous coil power and survives host resets, power cuts, and wiring interruptions. See Production Next Steps below.

Ignition-sense front end is POC-only: no transient protection. The bare 33 kΩ / 10 kΩ voltage divider does nothing against automotive electrical transients. Load-dump events on generator sets can produce transients 40–200 V above nominal 12 V, and starter-motor switching puts fast inductive spikes on the battery rail. Either one can exceed the host GPIO absolute maximum and damage the MCU. At a minimum, a production design must add a TVS diode (e.g. 15 V unidirectional, placed before the high-side divider resistor) and an RC low-pass filter (e.g. 1 kΩ + 100 nF at the A2 node), or use a properly isolated sensing front end. The relay side of the vehicle interface carries the same risk. The 1N4007 flyback diode protects the MOSFET drain from the relay coil's inductive spike, but it doesn't protect the 12 V feed from upstream transients, and the inline fuse in the BOM only addresses overcurrent.

No interrupt-driven ignition sense. The firmware polls the ignition state on each wake, so a thief who turns the key and drives away within the 2-minute window between wakes gets a head start before the ignition_on_immobilized event fires. A hardware interrupt on the ignition-sense line would wake the host the instant the key turns on. The Notecarrier CX's STM32 host can wake from STOP2 on a GPIO edge (an EXTI interrupt on A2, registered next to the existing ATTN wake with LowPower.attachInterruptWakeup()), but this POC doesn't implement it. The Notecard's AUX1–AUX4 pins aren't broken out on the Notecarrier CX, so the card.attn auxgpio approach isn't available on this carrier.

Inbound commands require cellular. The firmware doesn't register a compact template for immobilize.qi (or for environment variables), and the Notecard only syncs templated Notefiles over satellite. So commands from Notehub to the device need a cellular or WiFi connection to get delivered. If the equipment is already in a dead zone, the operator has to wait for cellular to come back before the command arrives.

Geofence decisions use cached location data, and daytime detection latency is bounded by heartbeat_stopped_min. The firmware calls card.location to read the Notecard's most recently cached GNSS fix instead of waiting for a fresh acquisition. The GNSS acquisition cadence bounds the cached fix age (card.location.mode seconds, default heartbeat_moving_s = 5 minutes), so one or two after-hours wake cycles may check the geofence against the same stale fix before GNSS re-acquires. A geofence breach alert won't appear until (a) the host wakes and (b) the Notecard has a fresh enough fix that puts the device outside the radius. During business hours, when the host wakes every 60 minutes (default), the worst-case detection latency for a theft that starts from a parked state is about 60 minutes plus up to 5 minutes of GNSS fix lag. Every outbound Note includes fix_age_s (seconds ≥ 0, or -1 when card.time is unavailable) so operators can judge how stale each geofence decision was.

Geofence is a circle. The firmware uses a Haversine-computed circle around a single center point. An irregular job-site boundary (an L-shaped lot, or equipment lots separated by roads) would need multiple overlapping circles or a polygon geofence, and this design implements neither.

Single geofence. The device stores one home location. Equipment that moves between job sites for legitimate reasons (morning deliveries, weekend staging) will trigger false geofence breach alerts unless the operator updates fence_lat / fence_lon in Notehub before each move.

No tamper detection. The firmware can't detect cable cutting, enclosure intrusion, or GPS jamming. A motivated thief who finds and removes the tracker, jams the antenna, or removes the equipment battery before the solar reserve is depleted will defeat this POC design.

Satellite data budget. Notecard for Skylo comes with 10 KB of bundled Skylo data, and that's a bundle, not a monthly allowance. Working from the actual template field types, the compact records come to 26 bytes per tracker.qo heartbeat (4 floats × 4 bytes + 5 int16 × 2 bytes) and 47 bytes per alert.qo event (4 floats × 4 bytes + 3 int16 × 2 bytes + 24-char string field at 25 bytes). On payload alone, the budget covers roughly 390 heartbeat-sized Notes (10,240 ÷ 26 bytes) or 215 alert-sized Notes (10,240 ÷ 47 bytes) in total. Satellite-session protocol and framing overhead bring the usable counts down from there, so validate against Notehub usage metrics and field measurements instead of relying on payload-only arithmetic. In normal stationary operation, heartbeats queue every hour, which comes to 24 × 26 = 624 bytes a day. Batching them into ~6 outbound sessions a day doesn't shrink that, so a unit that stays on satellite would use up the whole 10,240-byte bundle on heartbeats alone in about 16 days. Alert storm risk: if the equipment is under active pursuit and loses cellular coverage, repeated sync:true alerts every alert_cooldown_min (default 5 minutes) can burn through the Skylo budget fast. At 47 bytes × 288 alerts/day (5-min cadence) = 13.5 KB, a single day would use more than the whole bundled satellite allowance. If you expect satellite-only operation during a pursuit, raise alert_cooldown_min to 15–30 minutes with the Notehub fleet env var to protect the budget. In production, consider limiting satellite transmission to alerts only and enforcing a per-Note minimum satellite interval. Inbound checks count too. Each one costs about 50 bytes over satellite (Satellite Best Practices), so the 4-minute after-hours inbound_min cadence could spend most of the bundle in a single night on satellite, and commands can't arrive that way anyway. Set the _sync_inbound_mins_ntn reserved environment variable to a long interval (e.g. 1440) to give satellite its own, much slower inbound cadence.

Mojo is bench-only in this POC. The firmware doesn't read Mojo's LTC2959 coulomb counter over the Qwiic bus. Mojo only serves as a bench measurement instrument during validation.

Production Next Steps

Here's the engineering work that turns this POC into a production security device, in rough order from the most critical hardware change to fleet management.

A bistable (latching) relay replaces the standard relay. It's a dual-coil type, where one coil pulse sets the contact and a second pulse resets it, and it needs no continuous coil current to hold either state. Look for a dual-coil bistable mechanism, 12 V set and reset coils, a contact rating suited to the ignition/run-start control circuit, and an automotive-rated or equivalent environmental rating. The host drives it with single-pulse assertRelay / releaseRelay calls, and the relay contact state survives host sleep, power interruption, and wiring cuts.

Interrupt-driven ignition sense closes the polling gap. Register the ignition-sense pin (A2) as a second STOP2 wake source on the STM32 host with LowPower.attachInterruptWakeup(PIN_IGNITION_SENSE, isr, RISING, DEEP_SLEEP_MODE), next to the existing ATTN wake in cx_sleep.h. A key-on edge then wakes the host instantly instead of waiting for the next timer to expire. (The Notecard's AUX1–AUX4 pins aren't broken out on the Notecarrier CX, so card.attn auxgpio mode isn't an option here.)

Satellite data budgeting protects the bundled allowance. Track the cumulative satellite Note count in state, keep non-alert heartbeats off the satellite path, and enforce a minimum 15-minute satellite interval, even after hours.

GPS jamming detection catches an active countermeasure. If card.location keeps returning err while the Notecard accelerometer shows motion, log a gps_jamming_suspected alert.

Multi-geofence support handles equipment that moves for legitimate reasons. Store several geofence Notes in a .db Notefile, one site per Note with its lat/lon/radius, and have the firmware query all of them and treat itself as in-fence if it's inside any one.

A dedicated anti-tamper microswitch on the enclosure lid, wired to a spare host GPIO (also registered as a STOP2 wake source), sends an enclosure_opened Note if someone tries to physically remove the device.

Notecard Outboard DFU adds wireless firmware updates for the STM32 host. That's how you push updated geofence logic, new alert types, or threshold recipes to an entire fleet without a truck roll.

12. Summary

The fleet manager who used to find out about a stolen skid steer when the morning crew showed up now hears about it within minutes of the wheels moving, with an immobilizer command staged to fire on the thief's next key-on. A Notecarrier CX and Notecard for Skylo, a solar-trickled LiPo, and two wires into the ignition circuit turn the equipment into something you can get back. Hourly heartbeats cover the business day, two-minute scans after hours send an alert right away on motion, and a staged relay blocks ignition on the next key cycle the host observes. A production latching relay is the clear next step from there. When the equipment ends up where stolen equipment always ends up, in a rural stash yard, a shipping container, or a metal barn, Skylo satellite still gets the location and alert Notes out long after cellular has gone dark. That's the difference between a recovered skid steer and one that's already been exported.

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
    • Modules
    • Sensor reading strategy
    • Event payload design
    • Low-power strategy
    • Retry and error handling
    • Key code snippet 1: compact Note template definition
    • Key code snippet 2: immediate-sync alert
    • Key code snippet 3: relay assertion on key-on edge
    • Key code snippet 4: adaptive sleep until the Notecard raises ATTN
  • 8. Data Flow
  • 9. Validation and Testing
  • 10. Troubleshooting
  • 11. Limitations and Next Steps
    • Simplified for the POC
    • Production Next Steps
  • 12. Summary

Resources

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