Rail Car Condition & Interchange 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/rail_car_tracker/ so it can learn how the device works and help you adapt it to your hardware.
This project is a supply chain tracking reference design for leased freight and tank rail cars. A solar-powered Notecard for Skylo, Blues' all-in-one cellular and satellite module, works with an embedded STM32 host to monitor car-level conditions, meaning location, shock and impact events, and coupler state. TANK_CAR builds get two more readings from the same device. An Adafruit MPRLS (0–25 PSI absolute) measures pressure at a low-pressure vent-port fitting, and a waterproof DS18B20 probe takes a single-point lading temperature.
1. Project Overview
The problem. A leased tank car or intermodal flat leaves a chemical plant in Texas headed for a refinery in New Jersey. Along the way it crosses four railroads, spends three days in a classification yard in Tennessee, then moves overnight to a second yard before final delivery. The lessor has no idea where it is unless the lessee files an EDI interchange report. That report may or may not be accurate, and it says nothing at all about condition. It won't tell you whether the car took a hard coupling impact, whether a fitting pressure is trending down in a way that suggests a valve leak, or whether someone decoupled the car from its consist somewhere in Iowa at 2 AM. Without on-car telemetry, nobody gets a timely answer to any of that.
Rail car lessors run fleets worth hundreds of millions of dollars with almost no real-time visibility below the level of the interchange report. That gap leads to everything from underused assets (a car that sits in a yard for two weeks because nobody can find it) to safety incidents nobody discovers until after delivery.
Why Notecard for Skylo. Rail corridors are the textbook example of connectivity that doesn't follow population. For every mile of track through Chicago or Houston, there are fifty through Montana, the Appalachian plateau, or the Texas Panhandle, where cellular coverage ranges from thin to nonexistent. A device that relies on LTE alone will go dark for hours or days at a time, right in the stretches where a change in condition is most likely to go unnoticed. There are no access points for a rail car to join, so WiFi plays no part in this deployment.
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.
Notecard for Skylo (NOTE-NBGLWX) closes the coverage gap with a single M.2 module that carries LTE-M/NB-IoT/GPRS for cellular coverage and Skylo NTN (Non-Terrestrial Network) satellite for everywhere else. It handles the fallback from cellular to satellite on its own, so the host firmware never needs to know which transport is in use. Notes pile up in Notecard flash during the long dead zones between windows, then flush as soon as any transport opens. For assets that can spend days out of cellular range and weeks in a yard, the whole design depends on that queue-and-forward model.
This architecture lines up directly with Blues' supply chain tracking use case, where mobile assets cross connectivity boundaries unpredictably and there's no fixed infrastructure to rely on.
Deployment scenario. A weatherproof NEMA 4X enclosure bolts to the car's end-sill or side-sill. Power comes from a small rooftop solar panel through a solar LiPo charge controller, which safely charges a lithium-ion polymer battery. A Blues Scoop lithium-ion capacitor buffer sits inline between a 5 V regulated supply and the Notecarrier VBAT rail. It smooths out the high-current bursts that cellular transmission demands, and satellite transmission even more so. Without it, a modest LiPo under a weak winter sun can brown out the radio mid-session. The Skylo-certified LTE/NTN antenna (included with Notecard for Skylo) and a passive GNSS antenna mount flush to the roof with a clear view of the sky. A magnetically actuated reed switch attaches near the coupler knuckle, and a matching magnet mounts to the coupler structure next to it. On TANK_CAR builds, the MPRLS connects to a low-pressure vent or fitting port on the car body.
2. System Architecture
Device-side responsibilities. One constraint shapes all the work on the car
itself. Every 15 minutes the host gets a short window to do everything, and
then it has to get out of the way. The Notecarrier CX's STM32L433 host wakes
when the Notecard raises its ATTN pin at the end of a
card.attn sleep. It
reads its sensors, scores shock events, looks for coupler-state edges, checks
three alert conditions on standard builds (seven on TANK_CAR builds), and
queues Notes to the Notecard over I²C. As soon as that's done it goes back to
sleep in STM32 STOP2 (a few µA) with its RAM intact, so the state struct just
waits in memory until the Notecard's ATTN pin wakes the host for the next
sample.
Notecard responsibilities. Anything that has to think about the network
lives in the Notecard, not the host. The Notecard holds
Notes in its on-device queue and takes a GPS
position fix every five minutes (card.location.mode in periodic mode,
seconds: 300). GPS runs ungated until the first fix after installation, then
switches to motion-gated with threshold: 1 so a car sitting in a yard isn't
burning battery on GNSS. Outbound data syncs on a voltage-variable
hub.set schedule that
stretches the interval as the battery drains. Transport selection happens
entirely on the Notecard. Notecard for Skylo ships set to wifi-cell, which
never uses the satellite radio, so configureNotecard() also sends
card.transport
with method set to cell-ntn. If LTE-M can't reach a tower, the Notecard
switches to Skylo NTN and ships the queued Notes over satellite, and the
firmware never asks which path was used.
The Notecard also pulls down environment
variables
from the Blues Notehub cloud service, so
fleet-wide thresholds can be retuned without a truck roll.
Notehub responsibilities. Once a Note leaves the car, the Notecard's
embedded global SIM carries it over supported carriers worldwide to
Notehub. Notehub ingests and stores the events and
applies project-level
routes. The three
Notefiles (railcar_status.qo, railcar_alert.qo, railcar_location.qo) are
kept separate on purpose so each one can take its own downstream path. Status
Notes flow to a long-term analytics store for trend analysis. Alert Notes go
out to an on-call endpoint (email, SMS, webhook, CMMS ticket) in near real
time. Location Notes feed a geofencing service or time-series location store,
which is where interchange-boundary detection happens.
Routing (high level). Notehub supports HTTP, MQTT, AWS, Azure, GCP, and Snowflake routes. This project doesn't ship a specific downstream endpoint, so see the Notehub routing docs for setup. Smart Fleets can group cars by lessee, route, or car type (tank vs. flat) and route each fleet differently.
3. Technical Summary
What you'll have when you're done: a weatherproof, solar-powered electronics sidecar that mounts to a freight car. It samples sensors every 15 minutes, scores shock events, catches coupler state changes, and reports GPS location and condition through Notehub. Cellular gets used automatically when it's available, satellite takes over in remote areas, and everything queues offline until connectivity comes back.
Fastest path to first event (bench rig, ~1 hour):
- Create a Notehub project and copy the ProductUID (shown under Project Settings → ProductUID).
- Set
PRODUCT_UIDinfirmware/rail_car_tracker/rail_car_tracker_helpers.h(line 40: uncomment the#define PRODUCT_UIDline and replace the placeholder with your ProductUID). - Connect the ADXL345 accelerometer and reed switch to the Notecarrier
CX
over I²C/D5, and jumper
ATTNtoD9for host sleep (see §5 Wiring for the pinout). - Flash with
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/rail_car_tracker/ && arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/cu.usbmodem* firmware/rail_car_tracker/(the exact commands are in §7.1). Theusb=noneboard option turns off the USB CDC stack, which has to be off for the host to sleep. - Open the Notehub Devices tab, where the Notecard shows up within a few
minutes. Within 15 minutes you'll see
railcar_status.qowithcoupled,moving,shock_peak_g, andshock_windowsfields. §6 What you should see has sample JSON payloads.
Here is a sample Note this device emits:
{
"file": "railcar_alert.qo",
"body": {
"alert": "impact",
"value": 3.8,
"_lat": 41.4993,
"_lon": -81.6944,
"_ltime": 1713888000
}
}4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an onboard STM32L433 host, so you don't need a separate MCU. It has I²C, six analog inputs, nine digital I/O, and an M.2 Notecard slot. The host sleeps in STOP2 between samples until the Notecard's ATTN pin wakes it. See the Notecarrier CX datasheet. |
| Notecard for Skylo (NOTE-NBGLWX) | 1 | Single M.2 module with LTE-M/NB-IoT/GPRS + Skylo NTN satellite. It bundles 500 MB cellular + 10 KB satellite data, with no monthly fees. Ships with its Skylo-certified LTE/NTN antenna. Do not substitute another antenna without a CTIA/OTA delta test report, since Skylo may block uncertified devices. See the Notecard for Skylo datasheet. |
| Blues Mojo | 1 | Coulomb counter for bench power validation (see §9). It stays on the bench and isn't deployed to the field. See the Mojo datasheet. |
| Blues Scoop | 1 | Lithium-ion capacitor (250 F) peak-current buffer, wired inline between the 5 V boost module and the Notecarrier CX +VBAT rail. The Scoop has two connectors and two sets of header pins. J1 (CHG) is the JST input (4.8–24 V), with headers J3 as a through-hole alternative to J1. J2 (OUT) is the JST output (2.5–3.8 V), with headers J4 as a through-hole alternative to J2. The 5 V boost module output connects to J1/J3, and Scoop J2/J4 connects to Notecarrier CX +VBAT. The internal LiC charges from J1 between radio sessions and supplements the supply rail through J2 during high-current cellular and satellite bursts. J1 requires ≥ 4.8 V, so do not connect it directly to the LiPo (max 4.2 V) or to a raw solar panel. The Scoop is not a LiPo charger and doesn't replace the solar charge controller. See the Scoop datasheet. |
| 5 V boost regulator, 3.0–4.2 V input, 5 V regulated output, ≥ 500 mA (e.g., Pololu U1V11F5 or equivalent single-cell LiPo boost module) | 1 | Converts LiPo voltage (3.0–4.2 V) to the regulated 5 V that the Scoop J1 (CHG) input needs. Connect the LiPo JST to this module's input and the 5 V output to Scoop J1 (see §5). A 500 mA output rating gives enough current to recharge the Scoop's internal LiC between radio sessions. |
| Adafruit Triple-Axis Accelerometer ADXL345 (product 1231) | 1 | ±16 G, I²C, 3.3 V. It supplies raw G-force readings for shock scoring at ±16 G full resolution. Notecard's internal accelerometer tracks motion state but doesn't expose raw G values, so you need the ADXL345 to score impact magnitude. |
| Adafruit MPRLS Ported Pressure Sensor Breakout (product 3965) | 1 | TANK_CAR builds only (enable it by uncommenting #define TANK_CAR in rail_car_tracker_helpers.h). I²C, 3.3 V, 0–25 PSI absolute (0–1723 hPa), ±0.25 % FSS. It returns absolute pressure, so at sea level a port open to atmosphere reads about 14.7 PSI absolute. That's fine for monitoring fitting pressure on the car body where working pressure stays within the 0–25 PSI absolute range. Not rated for DOT-111, DOT-105, or higher-class tank pressure. For production cargo pressure monitoring, use a certified industrial transducer (see §11). Leave it off the BOM entirely for intermodal flat, boxcar, gondola, and other non-tank builds. |
| Adafruit DS18B20 Waterproof Digital Temperature Sensor (product 381) | 1 | TANK_CAR builds only. One-wire (OneWire protocol), 3.3–5 V, −55 to +125 °C, ±0.5 °C accuracy (−10 to +85 °C range). Configured at 12-bit resolution (0.0625 °C), it connects to pin D6 on the Notecarrier CX header. For single-point cargo temperature monitoring, run the probe through a sealed cable gland into the lading compartment. Check that the stainless-steel probe housing is chemically compatible with the lading before you install it. |
| 4.7 kΩ resistor, ¼ W, through-hole or SMD 0603/0805 | 1 | TANK_CAR builds only. Pull-up resistor for the DS18B20 one-wire data line, connected between the D6 data line and the +3V3_OUT rail (see §5). Standard 1 % or 5 % tolerance from any common supplier (Yageo, Bourns, Vishay, etc.) is fine. |
| Magnetic reed switch, N.O. contacts, weatherproof (e.g. Adafruit 375) | 1 | Mounts near the coupler knuckle and closes when the paired actuator magnet is in range, which is how the device tells coupled from decoupled. Adafruit 375 is sold by itself, so buy a compatible cylindrical rare-earth magnet separately (see next row). |
| Cylindrical rare-earth (neodymium) magnet, ≥ 10 mm diameter, weatherproof or epoxy-coated (e.g. Adafruit 9) | 1 | Actuator for the reed switch above. Mount it on the coupler pin, knuckle carrier, or a nearby structural member so the magnet is within the reed switch's rated operating distance when the coupler is closed. Check the magnet-to-switch operating distance against your switch's datasheet before mounting. |
| Solar charge controller, 6 V panel input, single-cell LiPo output, MPPT (e.g. CN3065-based module, 450 mA max charge) | 1 | Handles CC/CV LiPo charging from the solar panel with input-side MPPT. Never connect the solar panel directly to the LiPo terminal. The controller charges the LiPo, and a separate 5 V boost module (see row above) supplies the Scoop J1 input, so the charge controller itself doesn't need a USB/5 V output port. |
| Solar panel, 6 V, 3–5 W, rigid monocrystalline (e.g., Voltaic Systems P103C, 3.5 W) | 1 | Mounts flat on the car roof and feeds the charge controller's solar input. A 3–4 W panel is enough for the default 15-minute sample / 4-hour status cadence in a mid-latitude summer. Match it to your charge controller's maximum input current rating. |
| LiPo battery, 3.7 V single cell, 2000–4000 mAh (e.g., Adafruit 2011, 2000 mAh) | 1 | Primary energy storage. It connects to the charge controller battery output and to the 5 V boost module input. The Notecarrier CX gets its operating power through the Scoop J2 (OUT) connector, not directly from the LiPo. Size it to ≥2000 mAh for all-cellular corridors, and use 4000 mAh for mostly-satellite corridors or northern-latitude winter deployments. |
| GNSS magnetic-mount antenna, SMA, 3 m lead, 1575 MHz (L1), multi-band preferred (e.g., SparkFun GPS-14986) | 1 | Runs from the Notecard GPS u.FL port to the car roof through the u.FL-to-SMA adapter below. It must cover at least GPS L1 (1575.42 MHz), and multi-band L1/L2/L5 coverage (1164–1610 MHz) is preferred. Both antennas need a clear, unobstructed view of the sky. |
| u.FL (IPEX/MHF1) to SMA female bulkhead pigtail cable, 100–200 mm, RG178 coaxial | 1 | Adapts the Notecard GPS u.FL port to the SMA GNSS antenna above. Route the cable inside the enclosure, where the SMA female end mates to the antenna's SMA male connector through a panel-mount SMA bulkhead feedthrough in the enclosure wall. A 100 mm pigtail is typical. Keep the cable clear of sharp edges, and make sure the feedthrough is rated for the antenna's frequency range. |
| NEMA 4X polycarbonate enclosure, ~8×6×3″ | 1 | Weatherproof housing for all the electronics, rated for outdoor rail-car use. Use cable glands for the antenna leads, reed switch wire, pressure fitting, and power wiring. |
Notecard for Skylo ships with an active SIM that includes 500 MB of cellular data and 10 KB of satellite data, with no activation fees and no monthly commitment. The Notecarrier CX is a carrier board and doesn't contain a SIM.
5. Wiring and Assembly
All host I/O lands on the Notecarrier
CX dual 16-pin header.
Notecard for Skylo seats in the carrier's M.2 slot. Its MAIN u.FL port connects
to the included Skylo-certified LTE/NTN antenna, and its GPS u.FL port connects
to the separate GNSS antenna through the u.FL-to-SMA pigtail adapter listed in
the BOM. During bench testing the Mojo sits inline between Scoop J2 and the
Notecarrier +VBAT (take it out for field deployment).
Power chain (solar → charge controller → LiPo → 5 V boost → Scoop inline → Notecarrier):
- Solar panel
+/−→ charge controller solar input terminals. - Charge controller battery output JST → LiPo battery JST connector. The controller handles CC/CV charging and over-charge protection, so never connect the solar panel directly to the LiPo terminal.
- LiPo battery JST → 5 V boost module input. The boost module steps the LiPo's 3.0–4.2 V up to a regulated 5 V output.
- 5 V boost module output → Scoop J1 (CHG) JST connector (or the J3 through-hole header pins, which are electrically identical to J1). Scoop's CHG input needs ≥ 4.8 V, which the 5 V boost module provides. Do not wire J1 directly to the LiPo (max 4.2 V, below the 4.8 V minimum) or to the bare solar panel output.
- Scoop J2 (OUT) JST connector (or the J4 through-hole header pins,
electrically identical to J2) → Notecarrier CX
+VBATJST connector. J2 delivers 2.5–3.8 V, which is within the LiPo-range input the Notecarrier CX accepts. The internal LiC charges from J1 between radio sessions and discharges through J2 to supplement the supply rail during high-current cellular and satellite bursts. Scoop is the only path from the power chain to the Notecarrier+VBAT, and the LiPo never connects directly to the Notecarrier. - Mojo (bench only): Insert Mojo inline between Scoop J2/J4 and the
Notecarrier, wired as Scoop J2/J4 → Mojo
BATinput JST → MojoLOADoutput JST → Notecarrier CX+VBATJST. Then connect a Qwiic cable from either Mojo Qwiic port to a Qwiic port on the Notecarrier CX. Mojo reports cumulative mAh over Qwiic, and the Notecard detects it automatically (firmware v8 and later). The Mojo datasheet explains how that data shows up. Before field deployment, remove Mojo and restore the direct Scoop J2/J4 → Notecarrier CX+VBATconnection.
I²C bus (SDA / SCL pins on Notecarrier CX header):
The Notecarrier CX has on-board I²C pull-ups, and all the I²C devices share the bus without conflict:
| Device | I²C Address | Connection |
|---|---|---|
| Notecard for Skylo | 0x17 | Internal (M.2 slot) |
| ADXL345 breakout | 0x53 | SDA / SCL header; SDO pin to GND |
| MPRLS pressure sensor | 0x18 | SDA / SCL header; address is fixed (TANK_CAR builds only) |
Run a short Qwiic/STEMMA QT daisy chain or individual 4-wire (VCC/GND/SDA/SCL) connections from each breakout to the Notecarrier CX header.
1-Wire bus (DS18B20 cargo temperature probe, TANK_CAR builds only):
The DS18B20 uses a single-wire protocol on D6. Wire it like this:
- DS18B20 data (yellow) wire → Notecarrier CX
D6, and connect the 4.7 kΩ pull-up resistor (BOM item) from the data line to+3V3_OUT. - DS18B20 power (red) wire →
+3V3_OUT. - DS18B20 GND (black) wire →
GND.
Route the DS18B20's stainless-steel probe through a watertight cable gland in the enclosure wall and extend it into the lading compartment. The probe housing is rated for direct immersion, but check that it's chemically compatible with the specific lading before installation. Read §5.1 Safety before you plan any lading-compartment penetration.
Power to sensor breakouts:
- ADXL345
VCC→ Notecarrier CX+3V3_OUT. MPRLSVCC→+3V3_OUT(TANK_CAR builds only). All sensors together draw < 5 mA in normal use (100 mA available on+3V3_OUT). - All sensor
GNDpins → Notecarrier CXGND.
Reed switch (coupler state):
- One reed switch lead → Notecarrier CX
D5. - Other lead →
GND. - The firmware configures
D5asINPUT_PULLUP, so a closed reed switch (magnet present = coupled) pulls the pin LOW.
ATTN-to-D9 jumper (host sleep):
Fit a short jumper between the ATTN and D9 pins on the Notecarrier CX
header. Both are on the same 16-pin header. This design uses D9 because the
reed switch takes D5 and the DS18B20 takes D6, and the firmware sets
CX_ATTN_PIN to D9 before including cx_sleep.h. The Notecard pulls ATTN
low while the host sleeps and raises it when the next sample is due, and the
rising edge on D9 wakes the host from STOP2. Leave the Notecarrier CX EN
pin unconnected. On the CX, that pin 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.
MPRLS pressure port:
The MPRLS breakout has a small stainless-steel tube port on the sensor body (2.5 mm diameter, 3.7 mm long) made for slip-on tubing, not a threaded fitting. For field deployment, run short tubing from this port to a stainless-steel or brass fitting on a low-pressure vent or access port on the car body. Never connect it to a pressurized cargo line or any fitting that carries hazardous contents. Mount the sensor board inside the electronics enclosure, and run the fitting through the enclosure wall with a sealed bulkhead union. Make sure the fitting material is compatible with any vapors that may be present. Read §5.1 Safety before you plan any pressure port installation.
5.1 Safety Considerations
Read before installing. Ignoring the guidance below could lead to personal injury, property damage, regulatory violations, or exclusion from the Skylo network.
-
Certified antenna. Notecard for Skylo ships with a Skylo-certified LTE/NTN antenna. Any other antenna voids the Skylo certification, and Skylo may then block the device from its NTN network. Don't substitute another antenna unless you've obtained a delta test lab report through a CTIA/OTA-authorized facility. Contact Blues for test house recommendations.
-
Pressure port safety. The Adafruit MPRLS (product 3965) is a consumer-grade breakout with a 25 PSI absolute upper limit. It must not be plumbed into a pressurized cargo line, a fitting that carries hazardous or flammable contents, or any port that could exceed its 25 PSI absolute rating. Only qualified personnel should install a fitting or sensor on a regulated tank car, following all applicable DOT, AAR, and carrier/operator rules. For production cargo pressure sensing, replace the MPRLS with a certified industrial transducer rated for the lading and pressure class (see §11).
-
Solar power wiring. Never connect the solar panel output directly to the LiPo battery. Always route the panel through the specified charge controller (or an equivalent CC/CV controller rated for the panel and battery). Running a LiPo without proper charge control can cause fire, venting, or permanent battery damage.
-
Rail-car installation. All work on in-service rail cars must comply with applicable AAR, FRA, and carrier rules. This reference design is a bench proof-of-concept. Before you mount hardware to the car structure, route wiring through the car body, or connect to the car's fittings, you need authorization from the car owner or lessor, and the work must follow all applicable federal and industry regulations.
6. Notehub Setup
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID (which looks like
com.your-company.your-name:rail-tracker). Infirmware/rail_car_tracker/rail_car_tracker_helpers.h, uncomment the#define PRODUCT_UIDline and paste your ProductUID over the placeholder. -
Claim the Notecard. Power up the assembled unit. On first boot, Notecard for Skylo tries to connect over cellular and associates itself with the project automatically. The device shows up in the Devices tab within a few minutes. If there's no cellular coverage at the bench, move the unit, with its antenna connected, somewhere that has it. The first session has to go over cellular, since NTN only works after the templates have synced over a terrestrial link.
-
Set the device serial number. In Notehub, open the device and set the Serial Number field to the car's reporting mark and number (e.g.,
UTLX-123456). That value travels with every event, which makes it easy to filter events by car in downstream analytics. -
Create Fleets. Fleets group devices for shared configuration. For a rail car fleet, the natural boundaries are one fleet per lessee, one per car type (tank, flat, boxcar), or one per geographic corridor. Smart Fleets can use rule-based logic to assign devices automatically based on location or car metadata.
-
Set environment variables. In Notehub, go to Project → Fleets → [create or choose a fleet] → Environment, or to Project → Devices → [device] → Environment for per-device overrides. Every value below is optional, and the firmware defaults apply to anything you don't set. The device pulls changes on its next inbound sync, so you don't need to reflash.
Variable Default Purpose sample_interval_min15Minutes between sensor samples (and host wakes). report_interval_min240Minutes between railcar_status.qosummary Notes.location_interval_min30Maximum gap (minutes) between railcar_location.qoposition Notes while the car is moving. It doesn't affect motion-state-edge Notes, which fire on every wake that detects a stopped ↔ moving transition, whatever this interval is. Because the host only wakes on thesample_interval_mincadence, a transition that happens between wakes can be detected and reported up tosample_interval_minminutes later. Loweringsample_interval_minnarrows that window but costs battery life.shock_threshold_g2.5Peak resultant G at or above which an impact is counted and, after cooldown, an alert is sent. The 2.5 G default is a threshold on total resultant vector magnitude ( √(Gx²+Gy²+Gz²)), which includes the ~1 G static gravity component. The firmware doesn't apply gravity compensation or high-pass filtering, so the net dynamic impact this threshold represents depends on how the sensor is mounted relative to the impact direction. To interpret the value, calibrate empirically on each install using the baseline readings inrailcar_status.qo, not simple subtraction. Raise it for cars with heavy-duty draft gear, or lower it for sensitive cargo.shock_cooldown_min5Minimum minutes between consecutive shock alert Notes. This keeps a rough stretch of track from setting off an alert storm. pressure_max_psi20.0(TANK_CAR builds only.) Fitting absolute pressure (PSI) above which a pressure_highalert fires. Standard atmospheric pressure at sea level is ~14.7 PSI absolute, so set this threshold above the fitting's expected operating pressure. The firmware clamps this variable to 25 PSI to match the MPRLS absolute range.pressure_drop_psi10.0(TANK_CAR builds only.) If absolute pressure drops by more than this value (PSI) from the previous reading, a pressure_dropalert fires, since that can mean a leak or a sudden valve event.tank_temp_min_c-10.0(TANK_CAR builds only.) Cargo low-temperature alert threshold (°C). The tank_temp_lowalert fires when the DS18B20 probe reading falls below this value. The firmware clamps the threshold to the range −60–25 °C.tank_temp_max_c50.0(TANK_CAR builds only.) Cargo high-temperature alert threshold (°C). The tank_temp_highalert fires when the DS18B20 probe reading exceeds this value. The firmware clamps the threshold to the range 20–100 °C. -
Configure routes. Add one route for
railcar_alert.qo(to an on-call endpoint, CMMS, or webhook), a second forrailcar_status.qo(to a long-term analytics store), and a third forrailcar_location.qo(to a geofencing service or location time-series store). The Notehub routing docs list the supported route types. The three Notefiles are kept separate on purpose, so high-urgency alerts, periodic condition telemetry, and the dense position stream never share a routing path. You can throttle, transform, or forward each one independently.
What you should see in Notehub
In Devices → [device] → Events (or Project → Devices → Events, depending on which version of the UI you have), you'll see:
-
_session.qo: Notecard session housekeeping, one per cellular or NTN connection. It shows up first, within ~2–5 minutes on the bench if cellular is available, and confirms the radio is reaching Notehub. Blues only uses it for diagnostics, so you can ignore it. -
railcar_status.qo: sent right away on first boot, then once perreport_interval_min(default 240 minutes = 4 hours). In the sample body below, the GPS coordinates come from the Notecard's most recent fix through the_lat/_lon/_ltimecompact template fields, and they don't appear in the host-side payload:{ "_lat": 41.4993, "_lon": -81.6944, "_ltime": 1713888000, "coupled": true, "moving": false, "shock_peak_g": 1.3, "shock_windows": 0 }Field meanings:
coupledis the latest reed-switch state (true = magnet present, coupled).movingsays whether the Notecard's internal accelerometer detected motion in this sample window.shock_peak_gis the highest peak resultant G-force magnitude detected across all sample bursts since the previous summary (reported as 0.0 if the ADXL345 is missing or every read failed).shock_windowsis the count of sample windows whose peak exceededshock_threshold_g. It isn't a total impact count (see §7.3). TANK_CAR builds addpressure_psi(fitting absolute pressure) andtank_temp_c(cargo temperature) to this template. A value of-9999inpressure_psiortank_temp_cmeans the sensor didn't initialize on that wake, so treat it as a transient sensor fault. -
railcar_alert.qo: sent only when a threshold is exceeded, followed by an immediatehub.syncrequest. Thealertfield is one ofimpact,coupled,decoupled,pressure_high,pressure_drop,tank_temp_low, ortank_temp_high(the last four in TANK_CAR builds only). Here's a sample alert for a coupling impact:{ "alert": "impact", "value": 3.8, "_lat": 41.4993, "_lon": -81.6944, "_ltime": 1713888000 } -
railcar_location.qo: sent on two triggers. (1) A motion-state edge (stopped ↔ moving), detected on the host's wake cadence. Detection can lag the actual transition by up tosample_interval_minminutes, and once it's detected, the firmware requests ahub.syncright away in the same wake. (2) Everylocation_interval_minminutes while the car is moving (default 30 minutes). Sample body:{ "_lat": 41.4993, "_lon": -81.6944, "_ltime": 1713888000, "moving": true, "coupled": true }That gives a downstream geofencing service both the position and the operational context (moving/coupled) it needs to detect railroad boundary crossings and coupler changes at interchange points. The GPS coordinates come from the Notecard's last known fix, so the host doesn't have to query anything.
7. Firmware Design
Four-file sketch. All four files have to be in the same sketch directory to compile:
| File | Role |
|---|---|
rail_car_tracker.ino | setup() / loop() orchestration; global object definitions (Notecard, AppState) |
rail_car_tracker_helpers.h | Compile-time constants, #define TANK_CAR build flag, AppState struct, extern declarations, function prototypes |
rail_car_tracker_helpers.cpp | All Notecard interactions, sensor reads, and note-emission helpers |
cx_sleep.h | Host sleep: card.attn request, STOP2 entry, ATTN wake (CX_ATTN_PIN = D9 on this build) |
7.0 TANK_CAR build flag
A single #define in rail_car_tracker_helpers.h switches the firmware
between two build profiles:
// Uncomment the following line when deploying on a tank car fitted with an
// Adafruit MPRLS pressure sensor and DS18B20 cargo-temperature probe. Leave it
// commented out for intermodal flats, covered hoppers, gondolas, and other
// ...
// #define TANK_CAR
#ifdef TANK_CAR
#include <Adafruit_MPRLS.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#endifStandard build (default, TANK_CAR commented out) | TANK_CAR build | |
|---|---|---|
| Sensors initialized | ADXL345, reed switch | + MPRLS pressure sensor, DS18B20 cargo temperature probe |
Fields in railcar_status.qo | coupled, moving, shock_peak_g, shock_windows | + pressure_psi, tank_temp_c |
| Alert types | impact, coupled, decoupled | + pressure_high, pressure_drop, tank_temp_low, tank_temp_high |
| Env vars consumed | sample_interval_min, report_interval_min, location_interval_min, shock_threshold_g, shock_cooldown_min | + pressure_max_psi, pressure_drop_psi, tank_temp_min_c, tank_temp_max_c |
| BOM additions | None | Adafruit MPRLS breakout (product 3965), Adafruit DS18B20 waterproof probe (product 381), 4.7 kΩ pull-up resistor |
In the default standard build, the MPRLS is never initialized, the
pressure_psi field is left out of every Note template (which saves satellite
bytes), and the pressure alert logic is compiled out entirely. Non-tank assets
never log "MPRLS not found" warnings. Only enable TANK_CAR when the MPRLS
sensor is physically fitted.
Switching build profiles. The firmware encodes the build profile into
CONFIG_VERSION (standard = 4, TANK_CAR = 104), but the copy it compares
against, state.configVersion, lives in host RAM and starts at 0 on every
power-up. So any reflash, including one that toggles #define TANK_CAR, makes
defineTemplates() re-register the matching railcar_status.qo schema on the
first wake, with no manual version bump. That keeps the template from going
stale, either lacking pressure_psi and tank_temp_c or including them when
it shouldn't, which would make note.add reject payloads that don't match.
7.1 Installing and flashing
Dependencies:
- Arduino core for STM32:
stm32duino/Arduino_Core_STM32. Add the board index URLhttps://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.jsonunder File → Preferences → Additional Boards Manager URLs, then install "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 or witharduino-cli lib install "Blues Wireless Notecard". note-arduino releases lists the available versions.Adafruit MPRLS Library: TANK_CAR builds only. Install it from the Library Manager (arduino-cli lib install "Adafruit MPRLS Library"). Standard (non-tank) builds don't need it.OneWire: TANK_CAR builds only. Install it from the Library Manager (arduino-cli lib install "OneWire"). It provides the 1-Wire bus driver that the DS18B20 probe uses.DallasTemperature: TANK_CAR builds only. Install it from the Library Manager (arduino-cli lib install "DallasTemperature"). It's a high-level API for Dallas/Maxim DS18B20 temperature sensors over the OneWire bus.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.
The firmware drives the ADXL345 with direct I²C register reads through the
built-in Wire library, so it doesn't need another library.
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 arduino-cli:
# Install the STM32 core if you haven't already
arduino-cli core install STMicroelectronics:stm32
# Libraries (add "Adafruit MPRLS Library" OneWire DallasTemperature for TANK_CAR builds)
arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC"
# Compile. 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/rail_car_tracker/
# Upload (adjust /dev/cu.usbmodem* for your system; on Linux it may be /dev/ttyACM*)
# When the device appears in the list, use the actual port
arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/cu.usbmodem* firmware/rail_car_tracker/firmware/rail_car_tracker/sketch.yaml sets the same board options, so running
arduino-cli compile in that directory without -b picks them up too.
Serial console during a successful boot:
[init] templates and GPS/motion configured (v4)
[init] no GPS fix yet — running without motion gate for initial acquisition
[sample] coupled=1 peakG=0.90
[summary] sent
(host quiet for 15 minutes)
[init] initial GPS fix confirmed — motion gate enabled
[sample] coupled=1 peakG=1.10After the first sample cycle, the host sleeps in STOP2 until the Notecard raises
ATTN (15 minutes by default). When the serial output goes quiet, that's
expected behavior, not a hang. Use the timing shown above to check the
sample interval. Open a serial monitor at 115200 baud on the ST-LINK virtual
COM port from the Notecarrier CX debug jack (e.g.,
screen /dev/cu.usbmodem* 115200 on Mac/Linux, or the Arduino IDE's Serial
Monitor).
7.2 Modules
| Responsibility | Function |
|---|---|
| Notecard configuration | configureNotecard sends hub.set with voltage-variable sync and card.transport with cell-ntn, and returns bool |
| Note templates | defineTemplates registers compact templates for all three Notefiles and returns bool |
| Motion and GPS config | configureMotionAndGPS sets up the Notecard accelerometer + location mode and returns bool |
| Env var fetch | fetchEnvOverrides pulls and clamps all environment variables on every wake |
| Coupler debounce | readCouplerState takes a 5-sample majority vote |
| Shock scoring | adxl345Begin, adxl345ReadG, readPeakShockG: 64-sample burst, with each I²C read validated |
| Alert emission | sendAlert writes a compact Note and returns bool, and the sync is coalesced into one hub.sync after all alerts |
| Summary emission | sendSummary sends the latest sensor readings + shock window accumulators |
| Location emission | sendLocationNote writes a compact position Note to railcar_location.qo, fired on motion-state edges and on the location_interval_min cadence while moving |
Host sleep (STOP2 until the Notecard raises ATTN) | goToSleep → cxSleepUntilAttn (cx_sleep.h); return value checked, falls back to waiting awake |
7.3 Sensor reading strategy
- ADXL345 shock scoring. The sensor runs in ±16 G full-resolution mode
(3.9 mg/LSB). Each sample cycle reads 64 samples at ~100 Hz (a
~640-millisecond burst) and tracks the peak resultant vector magnitude
√(Gx²+Gy²+Gz²). At rest this reads ~1.0 G (static gravity), and an impact shows up as a spike above that baseline. The firmware compares total resultant magnitude, not gravity-compensated acceleration, so how the 2.5 G threshold relates to the net dynamic impact depends on how the sensor is mounted. You can't simply subtract the gravity vector's contribution from the resultant. The reliable way to setshock_threshold_gfor a specific installation is empirical per-install calibration, using the baseline readings you observe inrailcar_status.qo.shock_windowscounts sample windows, not individual impacts. The 640-millisecond burst runs once per 15-minute wake, so this firmware never sees an impact that happens between bursts.shock_windowsis therefore the number of wakes in the summary period where the burst peak exceededshock_threshold_g. It can be zero even when real impacts happened outside the sample windows, so don't read it as a total impact count. Each I²C read in the burst is validated. If the repeated-start write fails or fewer than 6 bytes come back, that sample gets thrown out instead of being allowed to produce a spurious magnitude spike. If every read in a burst fails,peakGreturnsNAN, and neither theshock_peak_gaccumulation norshock_windowsgets incremented for that cycle. - MPRLS. A single I²C measurement through the Adafruit library.
readPressure()returns pressure in hPa (absolute), and the firmware divides by 68.948 to convert to PSI absolute. The valid range is 0–25 PSI absolute (0–1723 hPa), with ±0.25 % FSS typical accuracy. At standard sea-level conditions a port open to atmosphere reads about 14.7 PSI absolute, and a pressurized fitting reads above that baseline. A sharp drop in absolute pressure toward atmospheric may point to a leak or valve event. If the reading sits well below atmospheric, a sensor wiring fault is the likely cause. This sensor isn't rated for full DOT-111 or higher-class tank pressure (see §11). - DS18B20 cargo temperature probe (TANK_CAR builds). The firmware
initializes it through the DallasTemperature library over the OneWire bus on
D6, at 12-bit resolution (0.0625 °C step). Each wake callsrequestTemperatures(), which blocks for about 750 milliseconds during the conversion, then reads the result withgetTempCByIndex(0). The library returnsDEVICE_DISCONNECTED_C(−127 °C) when the probe is missing or the wiring is broken. The firmware treats any value below −100 °C as an error and storesNAN, which gets reported as−9999intank_temp_c. Sensor accuracy is ±0.5 °C across the −10 to +85 °C range. Check that the stainless-steel probe housing is chemically compatible with the lading before installation. - Reed switch. Five
digitalReadsamples 20 milliseconds apart, with a majority vote (≥ 3 of 5 agreeing) deciding the accepted state. Comparing that state against the previous wake's (held in RAM through STOP2) catches edges, and each edge triggers a coupler-change alert.
7.4 Event payload design
All three Notefiles use compact Note
templates.
Satellite (NTN) transport requires them, and they cut the per-Note byte count
over cellular by a wide margin too. The reserved _lat, _lon, and _ltime
compact fields restore the GPS coordinates that a compact template would
otherwise strip. The Notecard fills in its most recent fix automatically, so
the host doesn't have to query GPS for summary Notes. The railcar_status.qo
template body takes up about 28 bytes in a standard build (add ~4 bytes each
for pressure_psi and tank_temp_c in TANK_CAR builds, for a total of ~36
bytes). That's the compact Note body size only, not the total satellite
data each Note consumes. Real NTN delivery also pays for session establishment,
routing metadata, and delivery receipts. §11
Limitations has guidance on the satellite
budget.
The railcar_status.qo body carries the latest sensor readings from the sample
cycle that triggered the summary. It also carries shock_peak_g (highest G
seen since the previous summary) and shock_windows (number of sample
windows in the summary period whose burst peak exceeded shock_threshold_g).
shock_windows counts sampled windows that crossed the threshold, not
individual impacts (see §7.3). So the body
holds the latest single reading plus accumulated extremes, not an average over
every sample in the window.
Here's a sample railcar_alert.qo body. The GPS coordinates come from the
Notecard's last known fix through the _lat/_lon/_ltime compact template
fields, and they don't appear in the host-side note.add call:
{
"file": "railcar_alert.qo",
"body": {
"alert": "impact",
"value": 3.8,
"_lat": 41.4993,
"_lon": -81.6944,
"_ltime": 1713888000
}
}Alert types and their value fields:
alert | value | Build |
|---|---|---|
impact | Peak resultant G of the triggering sample burst | All |
coupled | 1.0 (coupler closed) | All |
decoupled | 0.0 (coupler opened) | All |
pressure_high | Fitting absolute pressure (PSI) at alert time | TANK_CAR only |
pressure_drop | Magnitude of the absolute-pressure drop (PSI), not the current reading | TANK_CAR only |
tank_temp_low | DS18B20 cargo temperature (°C) that tripped the threshold | TANK_CAR only |
tank_temp_high | DS18B20 cargo temperature (°C) that tripped the threshold | TANK_CAR only |
7.5 Low-power strategy
Between samples the Notecarrier CX's STM32L433 host sleeps in STOP2, drawing a
few microamps with its RAM intact. setup() runs once at power-up to configure
the Notecard and arm the ATTN wake, and each pass through loop() is one
sample cycle. At the end of the cycle, goToSleep() calls cxSleepUntilAttn()
(in cx_sleep.h). That sends a card.attn sleep request for
sample_interval_min × 60 seconds, waits for the Notecard to pull ATTN low,
and puts the host into STOP2. When the interval is up, the Notecard raises
ATTN. The rising edge on D9 wakes the host, and execution picks up at the
top of loop(). Since the host resumes in place instead of cold-booting, the
AppState struct (alert latches, shock accumulators, configVersion) just
stays in RAM, and nothing gets serialized to the Notecard. The host is only
awake for the few seconds it takes to read sensors, check rules, and queue
Notes, which works out to roughly 5–10 seconds per 15-minute interval. If the
sleep request fails or ATTN never goes low (a missing jumper), goToSleep()
waits out the interval awake and tries again on the next cycle, so the sample
cadence holds either way.
Notecard for Skylo idles at ~8–18 µA @ 5V between sessions (see the low-power
firmware design
guide). GPS is
motion-gated. With threshold: 1, card.location.mode keeps the GNSS radio off
while the car sits in a yard and only turns it on when the Notecard's internal
accelerometer detects movement. The outbound sync cadence follows the battery's
charge state through the voutbound/vinbound voltage-variable strings:
| Voltage tier | voutbound interval | vinbound interval |
|---|---|---|
| USB | 60 minutes | 120 minutes |
| High | 120 minutes | 240 minutes |
| Normal | 240 minutes | 480 minutes |
| Low | 480 minutes | 720 minutes |
| Dead | Paused (0) | Paused (0) |
At high charge (good solar harvest) the Notecard syncs every 2 hours, at normal charge every 4 hours, and at low charge every 8 hours. At "dead" voltage it suspends outbound syncs to protect the battery. On first boot, the firmware sends a summary Note right away, whatever the report interval, so a known-good status lands in Notehub during commissioning.
7.6 Retry and error handling
- One-time configuration retry.
configureNotecard,defineTemplates, andconfigureMotionAndGPSall returnbool. The firmware retrieshub.setandcard.transporton every wake until both succeed, and it only setsstate.configVersiontoCONFIG_VERSIONonce the templates and GPS/motion config have both succeeded. If either step fails,configVersionstays the same, that wake skips Note emission, and the next wake retries the configuration sequence automatically. The first-wake handling (the commissioning summary, and refreshing the coupler and motion latches without alerting) waits for the first wake that gets through configuration, so a failed first attempt doesn't skip it. - Response error-field checking. Every
requestAndResponsecall checks for aNULLreturn and looks at theerrfield in the response JSON before treating the response as valid. Any non-NULL response is always paired withnotecard.deleteResponse.sendRequestcalls that return aboolget checked, and failures are logged. - Note emission error visibility.
sendAlertandsendSummaryuserequestAndResponse(not fire-and-forgetsendRequest), so the Notecard'serrfield is visible when something fails, and dropped Notes get logged to serial. - PRODUCT_UID runtime guard. If
PRODUCT_UIDis empty, the firmware logs an error to serial on every wake and goes straight back to sleep without queuing Notes or syncing. The misconfiguration is obvious right away at the bench, and you don't need Notehub to diagnose a missing project association. - Sleep failure.
cxSleepUntilAttn()returnsfalsewhen the Notecard doesn't accept thecard.attnrequest orATTNnever goes low (theATTN→D9jumper is missing).goToSleep()then logs a warning and waits out the sample interval awake withdelay(), which keeps the sample cadence, and the next cycle tries to sleep again. - Pressure drop validity. A separate
lastPressureValidflag inAppStatetracks whether the storedlastPressurePsicame from a successful sensor read, and the flag clears whenever a read returnsNAN. Apressure_dropalert only fires when both the previous and current readings are valid, so a stale reading can't manufacture a false drop alert after one or more failed cycles. - Summary timing stability. The firmware tracks the summary window as
accumulated elapsed minutes (
state.elapsedMin += sampleMinon each wake), not as a wake count multiplied by the current interval, so a runtime change tosample_interval_mindoesn't retroactively shift the window boundary. - MPRLS / DS18B20 fault sentinels. Reads that fail initialization return
NAN, andsendSummaryreplacesNANwith-9999inpressure_psiandtank_temp_cso downstream analytics can tell a sensor fault from a legitimate near-zero reading. The DS18B20 returnsDEVICE_DISCONNECTED_C(−127 °C) when the probe is missing or the wiring is broken. The firmware treats any value below −100 °C as an error and converts it toNANbefore callingsendSummary. If the ADXL345 is missing or every read in a burst fails,peakGisNAN, and neither theshock_peak_gaccumulation norshock_windowsgets incremented for that cycle. - Alert sync coalescing.
sendAlertandsendLocationNote(on motion-state edges) don't setsync:trueon individualnote.addcalls. Instead, once all the alert and location logic for a wake has run, the firmware issues a singlehub.syncif any alert or motion-edge location Note was queued. That avoids redundant sync-session requests when several events fire in the same wake. - Env-var clamping. Every value from
fetchEnvOverridesis clamped before use, so a malformed Notehub value can't produce an out-of-range sleep duration or a division-by-zero interval.
7.7 Key code snippet 1: compact template with GPS fields
NTN Notes on Notecard for Skylo require the format:"compact" argument. The
_lat/_lon/_ltime keywords restore the GPS coordinates that a compact
template would otherwise strip, and the Notecard fills in its most recent fix
automatically, so summary Notes don't need a host GPS query. FILE_STATUS is
railcar_status.qo, and only TANK_CAR builds add the pressure_psi and
tank_temp_c fields. defineTemplates() checks the response's err field and
returns false if registration fails.
J *req = notecard.newRequest("note.template");
JAddStringToObject(req, "file", FILE_STATUS);
JAddNumberToObject(req, "port", 10);
JAddStringToObject(req, "format", "compact");
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "_lat", TFLOAT32);
JAddNumberToObject(body, "_lon", TFLOAT32);
JAddNumberToObject(body, "_ltime", TINT32);
#ifdef TANK_CAR
JAddNumberToObject(body, "pressure_psi", TFLOAT32); // MPRLS absolute pressure (PSI)
JAddNumberToObject(body, "tank_temp_c", TFLOAT32); // DS18B20 cargo temperature (°C)
#endif
JAddBoolToObject (body, "coupled", true);
JAddBoolToObject (body, "moving", true);
JAddNumberToObject(body, "shock_peak_g", TFLOAT32);
JAddNumberToObject(body, "shock_windows", TINT16);
J *rsp = notecard.requestAndResponse(req);7.8 Key code snippet 2: voltage-variable hub.set
voutbound sets the outbound sync interval from the VBAT voltage the Notecard
reports, so the host doesn't need to measure the battery. At high charge the
Notecard syncs every 120 minutes, at normal every 240 minutes, and at low
every 480 minutes. At dead, outbound sync stops entirely. The same
configureNotecard() function then sets card.transport to cell-ntn, which
turns on the satellite fallback.
J *req = notecard.newRequest("hub.set");
JAddStringToObject(req, "product", PRODUCT_UID);
JAddStringToObject(req, "mode", "periodic");
// Sync every 4 h at normal solar voltage; stretch to 8 h when low;
// pause entirely at dead-battery to preserve modem startup energy.
JAddStringToObject(req, "voutbound",
"usb:60;high:120;normal:240;low:480;dead:0");
JAddStringToObject(req, "vinbound",
"usb:120;high:240;normal:480;low:720;dead:0");
bool ok = notecard.sendRequest(req);
if (!ok) debugSerial.println("[warn] hub.set failed");
// Notecard for Skylo ships set to wifi-cell, which never uses the
// satellite radio. cell-ntn prefers cellular and falls back to Skylo NTN.
req = notecard.newRequest("card.transport");
JAddStringToObject(req, "method", "cell-ntn");7.9 Key code snippet 3: ADXL345 shock scoring burst
Taking SHOCK_SAMPLES (64) reads at ~100 Hz gives a ~640-millisecond window,
which is long enough to catch a coupling impact transient. The resultant vector
magnitude includes the static gravity component (~1.0 G at rest). The firmware
doesn't compensate for gravity, so turning a 2.5 G threshold into a "net
impact" figure depends on the angle between the gravity vector and the impact
direction. To set shock_threshold_g for your installation, use empirical
per-install calibration, not simple arithmetic. Each I²C read is validated, and
failed reads are skipped so a short I²C transfer can't inject a spurious
magnitude. If no reads succeed, the function returns NAN.
float peakG = 0.0f;
uint8_t validSamples = 0;
for (uint8_t i = 0; i < SHOCK_SAMPLES; i++) {
float gx, gy, gz;
if (!adxl345ReadG(gx, gy, gz)) {
delay(10);
continue; // discard bad reads; do not feed error bytes into magnitude math
}
float mag = sqrtf(gx * gx + gy * gy + gz * gz);
if (mag > peakG) peakG = mag;
validSamples++;
delay(10); // ~100 Hz effective sample rate
}
return (validSamples > 0) ? peakG : NAN;7.10 Key code snippet 4: sleeping until the Notecard raises ATTN
cxSleepUntilAttn() asks the Notecard to hold ATTN low for exactly
sample_interval_min × 60 seconds, confirms the pin has gone low, and puts the
STM32 into STOP2. The host never powers off, so the card.attn call is a
request whose result gets checked, not a fire-and-forget command. The reed
switch and DS18B20 already take D5 and D6, so the sketch overrides the
default wake pin at the top of the file, before including cx_sleep.h.
goToSleep() runs at the end of every loop() pass, and setup() arms the
wake once with cxSleepBegin().
// ATTN is jumpered to D9 on this build (D5 = reed switch, D6 = DS18B20).
#define CX_ATTN_PIN D9
#include "cx_sleep.h"
// ...
static void goToSleep() {
uint32_t sleepSec = g_sampleMin * 60U;
if (!cxSleepUntilAttn(notecard, sleepSec, NULL, &debugSerial)) {
// The Notecard didn't take the sleep request, or ATTN never went low
// (check the ATTN -> D9 jumper). Keep the sample cadence and retry.
debugSerial.println("[warn] ATTN sleep failed — waiting out the interval awake");
delay(sleepSec * 1000UL);
}
}
// ...
// Arm the ATTN wake: the Notecard raises ATTN (D9) to end each sleep.
cxSleepBegin();8. Data Flow
Collected every sample_interval_min (default 15 min): coupler state
(boolean), peak resultant G in the sampling burst, and Notecard motion state
(moving/stopped from the internal accelerometer). TANK_CAR builds also collect
low-pressure fitting absolute pressure (PSI) and DS18B20 cargo temperature (°C).
Transmitted:
railcar_status.qo: one compact Note generated perreport_interval_min(every 4 hours by default, and also right away on first boot). Generation and delivery are separate steps. The STM32 host creates the Note and queues it to the Notecard on thereport_interval_mincadence, and the Notecard delivers queued Notes on the next outbound sync session. The voltage-variablehub.setschedules that session at 2 hours (high charge), 4 hours (normal charge), or 8 hours (low charge). When cellular is unavailable, delivery waits for the next time the Notecard can establish an NTN session with adequate sky view. The Note containscoupled,moving,shock_peak_g, andshock_windows, and TANK_CAR builds also includepressure_psiandtank_temp_c. The Notecard injects the GPS coordinates automatically from its most recent fix through the_lat/_lon/_ltimecompact template fields.railcar_alert.qo: sent on any threshold trip. Once all the alerts for a wake are queued, the firmware issues a singlehub.syncto request immediate delivery. The Notecard transmits over cellular if it can. Otherwise the Note waits in flash until the next satellite NTN window or the next time cellular coverage opens up.railcar_location.qo: sent on two independent triggers. (1) A motion-state edge (stopped ↔ moving), detected on the host'ssample_interval_minwake cadence. A transition that happens between wakes gets detected and reported on the next wake, up tosample_interval_minminutes later. Once it's detected, the firmware requests ahub.syncright away in the same wake, so the yard-arrival or yard-departure Note reaches Notehub without waiting for the next scheduled outbound window. (2) Everylocation_interval_minminutes while the car is moving (default 30 min, adjustable via env var). These fill the gap between periodic status summaries with a position record dense enough to work out interchange boundaries. The body contains onlymovingandcoupled, and the Notecard injects_lat/_lon/_ltimeautomatically from the last known GPS fix. GNSS runs every 5 minutes while the car is moving (motion-gated to save battery during yard dwell), so in-motion position fixes stay fresh well within the default 30-minute location cadence.
Routed: all three Notefiles land in Notehub. From there, railcar_alert.qo
goes to a real-time endpoint (webhook, email, or CMMS), and railcar_status.qo
goes to a time-series store for trend analysis and car utilization reporting.
railcar_location.qo goes to a geofencing service or location time-series
store for interchange-boundary detection.
Alert triggers (three always-on + four TANK_CAR-only):
| Alert | Condition | Repeat behavior | Build |
|---|---|---|---|
impact | Peak G ≥ shock_threshold_g | Rate-limited to one alert per shock_cooldown_min window, which prevents alert storms on rough track | All |
coupled / decoupled | Coupler-state edge from previous sample | Edge-detected, so one alert per state transition no matter how long the state lasts | All |
pressure_high | Fitting absolute pressure > pressure_max_psi | Edge-detected. Fires once when the condition first appears and stays quiet while it persists | TANK_CAR only |
pressure_drop | Fitting pressure fell > pressure_drop_psi since the previous sample | One alert per detected sudden-drop event. Suppressed unless both the previous and current readings are valid, so stale readings can't produce false alerts after a failed cycle | TANK_CAR only |
tank_temp_low / tank_temp_high | DS18B20 cargo temperature outside tank_temp_min_c / tank_temp_max_c | Edge-detected. Fires once when the condition first appears, stays quiet on later samples while it persists, and re-arms when the temperature comes back inside the range | TANK_CAR only |
9. Validation and Testing
Expected steady-state cadence. A correctly installed unit generates one
railcar_status.qo every report_interval_min (4 hours by default) and
delivers it on the next scheduled sync session. At normal battery voltage over
cellular, generation and delivery both run on a 4-hour cadence. At low voltage
the sync interval stretches to 8 hours, and Notes may sit in the queue that
long before delivery. railcar_location.qo fires on every detected
motion-state edge and, while the car is moving, every location_interval_min
minutes (default 30 min). The host detects motion-state edges on its 15-minute
wake cadence, so a stopped ↔ moving transition can be reported up to
sample_interval_min minutes after it happens. Once it's detected, the
firmware requests a hub.sync right away in the same wake. To shrink the
detection window, lower sample_interval_min (via env var), at the cost of
battery life. Zero railcar_alert.qo events is normal during smooth transit.
During initial commissioning, look at the shock_peak_g values in the first
several railcar_status.qo Notes to establish the car's baseline vibration
signature. Then tune shock_threshold_g and the pressure-drop threshold to
match. The firmware doesn't do automatic calibration or baseline learning, so
tuning thresholds at commissioning time is a manual step based on observed
data.
Power validation with Mojo. The Mojo sits inline on the VBAT rail during bench bring-up and reports cumulative mAh to the Notecard over Qwiic (see §5 for the full bench wiring).
The table below lists per-phase reference figures from the NOTE-NBGLWX datasheet and the Notecard low-power design guide, next to whole-system bench estimates for this assembly. Read the two columns differently. The reference column is what Blues has published, and the bench estimate column is a starting point for you to confirm with Mojo.
| Phase | Reference figure | Source | Whole-system bench estimate |
|---|---|---|---|
| Deep sleep: host in STOP2, Notecard idle (radio off) | ~10–20 µA @ 5V (Notecard ~8–18 µA + host ~1–2 µA) | Low-power design guide | ~20–50 µA total @ 3.7 V |
| Sample cycle: host + sensors active (~5–10 s) | n/a | n/a | ~30–40 mA |
| Cellular sync, average current | Modem active: ~250 mA | NOTE-NBGLWX datasheet | ~150–300 mA average, with a typical session of ~15–60 s |
| Cellular sync, burst peak | Up to ~2 A for a few ms (GSM fallback regions) | NOTE-NBGLWX datasheet | The battery, connectors, wiring, charge path, and Scoop buffer must all be validated against the full published peak burst-current envelope (~2 A). Check them with Mojo traces before finalizing sizing, and don't use a headroom figure below the peak you cite |
| NTN satellite sync (Skylo session), average current | No NTN-specific figure is published for the NOTE-NBGLWX. Modem-active baseline: ~250 mA | NOTE-NBGLWX datasheet | Bench estimate only: sustained current during a session is likely at or above the cellular average, and sessions last minutes. Validate with Mojo before finalizing sizing |
| NTN satellite sync, burst peak | Not published for NOTE-NBGLWX NTN operation | n/a | Size for at least the same headroom as cellular, and treat any pre-Mojo estimate as a floor, not a ceiling |
Estimated daily energy budgets (whole-system estimates only, so always measure with Mojo before finalizing battery and panel sizing):
| Scenario | Daily consumption | Notes |
|---|---|---|
| All-cellular, mostly-stationary (car in yard) | 15–25 mAh / 24 h | GNSS motion-gated off. Host sleeps 15 min / wake cycle for ~5–10 s. Sync every 4 h at normal charge. Best-case baseline for yard-dwell scenarios. |
| All-cellular, in-transit (car moving hours/day) | 40–80 mAh / 24 h | GNSS active every 5 min while moving. railcar_location.qo fires every 30 min + on motion edges. The GNSS duty cycle adds a lot to the budget, so validate it with Mojo under a representative duty cycle. |
| Mixed NTN/cellular or predominantly NTN | 80–200+ mAh / 24 h | NTN session overhead dominates. Sessions last 1–4 min at high current, and the exact budget depends on coverage in your corridor. Measure with Mojo in your target deployment zone before sizing. |
Commissioning guidance for 2000 mAh LiPo + 3.5 W solar panel (US mid-latitude, summer): A stationary car in a yard should run indefinitely on a 3.5 W panel and 2000 mAh battery, because daytime solar recharges faster than the baseline drain. An in-transit car burning 50 mAh/day should also keep going on the same panel in good summer sun. In winter or at higher latitudes, step up to a 5–6 W panel or a 4000 mAh battery. On NTN-heavy corridors, validate consumption with Mojo before final sizing, since satellite sessions can take over the budget without warning.
Mojo trace patterns to look for:
- Healthy: flat near-zero baseline with brief blips at the 15-minute sample interval and one longer burst (cellular or satellite) at the sync interval.
- Host not sleeping: a continuous 30–50 mA baseline means the host isn't
entering STOP2. Check the
ATTN→D9jumper on the Notecarrier CX header (the firmware logs[warn] ATTN sleep failedwhenATTNnever goes low). Also confirm the firmware was built with USB support set to None (usb=none), since with USB CDC enabled the host leaves STOP2 within a millisecond. - Frequent satellite sessions: very long (1–4 min) high-current bursts in place of the shorter cellular bursts. That's normal when the car is in a cellular-dark corridor.
- Rapid satellite data depletion: look at how often wakes request a
hub.sync(every wake that queues an alert or a motion-edge location Note does), since each satellite packet bills at least 50 bytes. Notes without a compact template never go out over NTN, so they can't be the cause.
Mojo is a bench-validation and per-firmware-revision regression tool, and field units don't need it.
10. Troubleshooting
| Symptom | Diagnosis | Recovery |
|---|---|---|
| Device does not appear in Notehub Devices tab after 5 minutes | PRODUCT_UID isn't set, or it's set incorrectly. The Notecard claims itself to a ProductUID on first boot over cellular. | Check line 40 of firmware/rail_car_tracker/rail_car_tracker_helpers.h, make sure the #define PRODUCT_UID line is uncommented, and confirm its value matches Project Settings → ProductUID in Notehub, then re-flash. If cellular is unavailable at the bench, the Notecard can't claim itself, and NTN can't stand in, because the templates have to sync over cellular before NTN works. Connect the LTE/NTN antenna and move near a window or outdoors until you get a cellular connection. |
Serial monitor shows [error] PRODUCT_UID is empty on every wake and no Notes are queued | PRODUCT_UID is empty or commented out. | Uncomment and set the ProductUID in rail_car_tracker_helpers.h, then re-flash. |
_session.qo appears but no railcar_status.qo for 20+ minutes | The host isn't waking, isn't queuing Notes, or hasn't sent the first-boot status Note yet. | Check three things. (1) Make sure the host is entering sleep. The serial monitor should show [sample] once, go quiet for ~15 minutes (normal), then show [sample] again. If the output is continuous, the host isn't sleeping, so check the ATTN→D9 jumper and the usb=none build option (the log shows [warn] ATTN sleep failed when cxSleepUntilAttn() can't sleep). (2) Make sure the Notecard is configured. On first boot the log should show [init] templates and GPS/motion configured, with no [warn] hub.set failed or [warn] card.transport failed before it. Anything that fails retries on the next wake. (3) Check the report interval. The firmware sends a status Note on the first wake that gets through configuration. If the log shows [init] config incomplete, that Note waits for the wake where configuration succeeds, and after it the next one comes report_interval_min later (240 minutes by default). For faster feedback during commissioning, drop report_interval_min to 15 through Notehub Fleet → Environment. |
railcar_status.qo has shock_peak_g: 0.0 always | The ADXL345 isn't connected or isn't responding on I²C, so check the SDA/SCL wiring to the header. The firmware logs [warn] ADXL345 not found at startup, and on each wake after that, if it can't find the sensor. While the sensor is missing, shock is recorded as 0.0. | Check the Qwiic/STEMMA QT cable to the ADXL345 breakout, or the 4-wire I²C connections (VCC/GND/SDA/SCL). Re-flash and watch the serial output. |
railcar_status.qo has pressure_psi: -9999 (TANK_CAR build) | The MPRLS pressure sensor failed to initialize or returned an error on this wake. | Check the SDA/SCL wiring to the MPRLS breakout, and confirm the I²C address is 0x18 (the default, since the Adafruit 3965 has no address pin jumpers). Power-cycle the unit. If the errors continue, check that TANK_CAR is enabled in rail_car_tracker_helpers.h and the MPRLS library is installed (arduino-cli lib install "Adafruit MPRLS Library"). |
railcar_status.qo has tank_temp_c: -9999 (TANK_CAR build) | The DS18B20 probe failed to initialize, or the wiring is broken. The firmware treats values below −100 °C as errors. | Make sure the 4.7 kΩ pull-up resistor is connected between D6 and +3V3_OUT. Check the DS18B20 data (yellow) wire to D6, power (red) to +3V3_OUT, and GND (black) to GND, then power-cycle. If the probe has been run too long or the stainless-steel housing is corroded, replace the sensor. |
| Device claims but never syncs (no Notes flow to Notehub) | Cellular and satellite coverage are both unavailable, so Notes queue in Notecard flash and wait for a window. Also check the hub.set configuration. | Check the antenna connections. Notecard for Skylo's MAIN u.FL must connect to the Skylo-certified LTE/NTN antenna (included with the Notecard), and the GPS u.FL connects to the separate GNSS antenna through the u.FL-to-SMA pigtail. Both antennas need a clear view of the sky. If both are connected and the sky view is clear, look for [hub] errors in the Notecard console in Notehub (Devices → [device] → Notecard Console). |
| Mojo shows high baseline current (30–50 mA continuously) | The host isn't sleeping. It only enters STOP2 when the Notecard pulls ATTN low on the jumpered D9 pin and the firmware was built with USB CDC disabled. If something is holding the host awake, the baseline won't drop to sleep levels (~20–50 µA). | Check the serial log for [warn] ATTN sleep failed, which means ATTN never reached D9, and make sure the ATTN→D9 jumper is seated (see §5). If the log shows the host sleeping but current stays high, confirm the build used USB support: None (usb=none). A host that never sleeps draws ~30–50 mA continuously. |
| Rapid satellite session bursts (1–4 minutes high-current events) | Cellular coverage is gone, so the Notecard is falling back to Skylo NTN. That's expected, correct behavior. | No action needed, since this is the reference design working as intended. Watch satellite data consumption in Notehub to make sure you're within your NTN budget. Keep in mind that Notes without a compact template never sync over NTN, and they wait in the queue for cellular. |
11. Limitations and Next Steps
A car-level tracker has to live for years on solar power, ride out weeks in cellular-dark corridors, and report condition without false alarms, and that leaves a lot to tune. What follows marks the scope of this reference design. It covers the places where we kept the implementation simple so the queue-and-forward architecture stays easy to read, plus the natural extensions for a fleet-grade deployment.
Simplified for this POC
Each item below is a place where this build stays simple, along with the tradeoff a production deployment should weigh.
The satellite data budget cannot be projected from body sizes. Compact Note
templates shrink each Note's payload a lot. railcar_status.qo takes about 28
bytes of body content per Note (add ~8 bytes for pressure_psi and
tank_temp_c in TANK_CAR builds, for a total of ~36 bytes), railcar_alert.qo
about 24 bytes, and railcar_location.qo about 20 bytes. Those are
body-only, template-only sizes, though, and they don't represent end-to-end
satellite data consumption. Real Skylo NTN usage also includes
session-establishment overhead, routing metadata, delivery receipts, and any
retries. Session overhead can dominate the budget well before raw body bytes
matter, especially at frequent sync cadences or when alert traffic is high.
Do not use body-size arithmetic to project allowance endurance. Before you
size a production satellite plan, check actual satellite byte consumption in
Notehub under your intended sync cadence and expected alert behavior. In
practice, a car on a US rail corridor spends much of its time in cellular range
at yards and along populated corridors, which saves most of the 10 KB bundled
allowance for the truly remote stretches.
The shock threshold is total vector magnitude, not gravity-compensated. At
rest, the resultant magnitude reads ~1.0 G (static gravity), and the 2.5 G
threshold applies to the total √(Gx²+Gy²+Gz²). The firmware doesn't apply
gravity compensation or high-pass filtering, so you can't turn this threshold
into a "net impact" figure by simple subtraction. The relationship depends on
the angle between the gravity vector and the impact direction. Making the
threshold orientation-independent would take gravity compensation or high-pass
filtering in firmware, and the alternative is empirical per-install
calibration. Cars with active vibration (e.g., empty tank cars resonating on
corrugated track) may need the threshold raised to 3–4 G to suppress nuisance
alerts. Tune it with the shock_threshold_g env var after you've watched the
baseline readings in railcar_status.qo.
Cargo temperature is single-point only (TANK_CAR builds). The DS18B20 measures temperature at a single point, the probe tip. Ladings with large internal temperature gradients, or cases where regulations require multi-point temperature verification, need more probes or a certified cargo temperature system. The probe's accuracy (±0.5 °C, −10 to +85 °C range) is enough for basic thermal monitoring of most common bulk liquid ladings, but you should validate it against your specific lading and temperature range.
Pressure monitoring is low-pressure fitting only; there is no tank gauge
pressure. The Adafruit MPRLS covers 0–25 PSI absolute (roughly 0–10 PSI above
atmospheric at sea level). The firmware clamps the pressure_max_psi env var
to 25 PSI to match that absolute range. DOT-111 non-pressure cars can operate at
up to ~100 PSI gauge, and DOT-105 and pressure cars go higher still. All of
them need a certified industrial pressure transducer of the right type (gauge,
absolute, or differential) with a wetted-parts spec that matches the lading.
Solar power requires a charge controller. The solar charge controller in the BOM is a required component. Do not substitute a direct panel-to-LiPo connection.
Coupler state is sampled, not interrupt-driven. If a coupling or decoupling
event happens and reverses entirely within a single 15-minute sample window,
the firmware won't see it. Dropping sample_interval_min to 5 minutes narrows
that gap, at the cost of battery life.
Interchange detection is not implemented on-device. Interchange tracking comes down to working out when a car has changed railroad custody. That takes either on-device geofencing against railroad territory boundary polygons, or downstream processing against a geospatial database of those boundaries. This firmware does neither. It reports GPS location, motion state, coupler state, and sensor readings, but it has no handoff-detection logic. Production interchange tracking also means feeding detected boundary crossings to the AAR's Umler or Railinc EDI platforms as structured interchange transactions. The geofencing step and the EDI integration step are both production work outside the scope of this POC (see Production Next Steps below).
There is no GNSS in covered yards. A car parked under an overhead conveyor structure or inside a building loses its GPS fix. The Notecard keeps the last known position, but location accuracy degrades until the car can see the sky again.
Production Next Steps
Getting from this reference build to fleet-grade condition reporting and formal interchange integration means picking up the work below.
Field-calibrate shock_threshold_g per car type. A tank car with its hand
brake applied resonates differently from an empty flatcar, so a single
fleet-wide threshold is only a starting point.
Add additional DS18B20 probes on the same OneWire bus (up to ~10 devices per
bus segment) for multi-point cargo temperature profiling. getDeviceCount()
and getTempCByIndex(n) in the DallasTemperature library let you iterate over
all the sensors.
Replace the MPRLS with a 4–20 mA industrial pressure transmitter (e.g., Honeywell STS3000 series) rated for the lading and pressure class, and read it through a ~150 Ω burden resistor on an analog input (20 mA × 150 Ω = 3.0 V, inside the host's 3.3 V ADC range).
Wire a second reed switch to a hatch or valve cover on tank cars to catch unauthorized access to the dome.
Implement Notecard Outboard DFU so you can push threshold recipe updates and new alert rules to the fleet over the air, without a truck roll.
Connect Notehub location events to a geofencing service or location time-series store (via a Notehub route) to detect railroad territory boundary crossings and generate interchange events for each custody handoff.
Feed those interchange events to an AAR/Railinc EDI gateway (Umler or Railinc platforms) for automated interchange reporting.
12. Summary
The lessor who used to wait on an EDI interchange report now hears from the tank car itself. That same Texas-to-New-Jersey trip, three days of it in a Tennessee yard with nothing to show but a paperwork acknowledgment, now produces a quarter-hourly health check, a continuous GPS track, a peak-G impact score for any hard hit that lands in a sample burst, and an alert the moment fitting pressure drops or temperature drifts out of range. The continuous GPS track, coupler-state transitions, and motion events that reach Notehub are exactly what a downstream geofencing service needs to detect railroad boundary crossings and generate interchange records. Turning that telemetry into formal interchange events takes geofencing and EDI integration, which is production work outlined in §11 Production Next Steps. Status Notes are generated every 4 hours and delivered on the next sync session. Over cellular at good charge that's within 2–4 hours, and out in the open Nebraska Sandhills it's whenever the Notecard can next establish an NTN session. Notecard for Skylo never asks the firmware to choose a transport. It manages cellular and satellite on its own, queues everything that can't go out right away, and flushes it as soon as connectivity comes back. For an asset class where "no news" has long meant "no visibility," that queue-and-forward guarantee is the whole point.
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.0 TANK_CAR build flag
- 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: compact template with GPS fields
- 7.8 Key code snippet 2: voltage-variable hub.set
- 7.9 Key code snippet 3: ADXL345 shock scoring burst
- 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