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_rightRail Car Condition & Interchange Tracker

Rail Car Condition & Interchange Tracker

Rail Car Condition & Interchange Tracker 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/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.

System architecture: on-car sensors → Notecarrier CX with STM32 host + Notecard for Skylo → LTE-M/NB-IoT + Skylo NTN satellite → Notehub → routes

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):

  1. Create a Notehub project and copy the ProductUID (shown under Project Settings → ProductUID).
  2. Set PRODUCT_UID in firmware/rail_car_tracker/rail_car_tracker_helpers.h (line 40: uncomment the #define PRODUCT_UID line and replace the placeholder with your ProductUID).
  3. Connect the ADXL345 accelerometer and reed switch to the Notecarrier CX over I²C/D5, and jumper ATTN to D9 for host sleep (see §5 Wiring for the pinout).
  4. 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). The usb=none board option turns off the USB CDC stack, which has to be off for the host to sleep.
  5. Open the Notehub Devices tab, where the Notecard shows up within a few minutes. Within 15 minutes you'll see railcar_status.qo with coupled, moving, shock_peak_g, and shock_windows fields. §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

PartQtyRationale
Notecarrier CX1Integrated 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)1Single 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 Mojo1Coulomb counter for bench power validation (see §9). It stays on the bench and isn't deployed to the field. See the Mojo datasheet.
Blues Scoop1Lithium-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)1Converts 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)1TANK_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)1TANK_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/08051TANK_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)1Mounts 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)1Actuator 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)1Handles 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)1Mounts 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)1Primary 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)1Runs 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 coaxial1Adapts 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″1Weatherproof 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).

Wiring diagram: ADXL345 and reed switch on I²C/D5, MPRLS and DS18B20 (TANK_CAR), ATTN → D9 jumper so the Notecard can wake the host from STOP2, antennas on u.FL ports, and the inline Scoop power path: solar → charge controller → LiPo → 5 V boost → Scoop J1 (CHG, ≥4.8 V) → Scoop J2 (OUT, 2.5–3.8 V) → Notecarrier CX +VBAT

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 +VBAT JST 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 BAT input JST → Mojo LOAD output JST → Notecarrier CX +VBAT JST. 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 +VBAT connection.

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:

DeviceI²C AddressConnection
Notecard for Skylo0x17Internal (M.2 slot)
ADXL345 breakout0x53SDA / SCL header; SDO pin to GND
MPRLS pressure sensor0x18SDA / 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. MPRLS VCC → +3V3_OUT (TANK_CAR builds only). All sensors together draw < 5 mA in normal use (100 mA available on +3V3_OUT).
  • All sensor GND pins → Notecarrier CX GND.

Reed switch (coupler state):

  • One reed switch lead → Notecarrier CX D5.
  • Other lead → GND.
  • The firmware configures D5 as INPUT_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

warning

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

  1. 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). In firmware/rail_car_tracker/rail_car_tracker_helpers.h, uncomment the #define PRODUCT_UID line and paste your ProductUID over the placeholder.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

    VariableDefaultPurpose
    sample_interval_min15Minutes between sensor samples (and host wakes).
    report_interval_min240Minutes between railcar_status.qo summary Notes.
    location_interval_min30Maximum gap (minutes) between railcar_location.qo position 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 the sample_interval_min cadence, a transition that happens between wakes can be detected and reported up to sample_interval_min minutes later. Lowering sample_interval_min narrows 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 in railcar_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_high alert 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_drop alert 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_low alert 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_high alert fires when the DS18B20 probe reading exceeds this value. The firmware clamps the threshold to the range 20–100 °C.
  6. Configure routes. Add one route for railcar_alert.qo (to an on-call endpoint, CMMS, or webhook), a second for railcar_status.qo (to a long-term analytics store), and a third for railcar_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 per report_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/_ltime compact 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: coupled is the latest reed-switch state (true = magnet present, coupled). moving says whether the Notecard's internal accelerometer detected motion in this sample window. shock_peak_g is 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_windows is the count of sample windows whose peak exceeded shock_threshold_g. It isn't a total impact count (see §7.3). TANK_CAR builds add pressure_psi (fitting absolute pressure) and tank_temp_c (cargo temperature) to this template. A value of -9999 in pressure_psi or tank_temp_c means 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 immediate hub.sync request. The alert field is one of impact, coupled, decoupled, pressure_high, pressure_drop, tank_temp_low, or tank_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 to sample_interval_min minutes, and once it's detected, the firmware requests a hub.sync right away in the same wake. (2) Every location_interval_min minutes 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:

FileRole
rail_car_tracker.inosetup() / loop() orchestration; global object definitions (Notecard, AppState)
rail_car_tracker_helpers.hCompile-time constants, #define TANK_CAR build flag, AppState struct, extern declarations, function prototypes
rail_car_tracker_helpers.cppAll Notecard interactions, sensor reads, and note-emission helpers
cx_sleep.hHost 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>
#endif
Standard build (default, TANK_CAR commented out)TANK_CAR build
Sensors initializedADXL345, reed switch+ MPRLS pressure sensor, DS18B20 cargo temperature probe
Fields in railcar_status.qocoupled, moving, shock_peak_g, shock_windows+ pressure_psi, tank_temp_c
Alert typesimpact, coupled, decoupled+ pressure_high, pressure_drop, tank_temp_low, tank_temp_high
Env vars consumedsample_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 additionsNoneAdafruit 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.

note

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 URL https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json under 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 with arduino-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 dependency STM32duino RTC): stm32duino/STM32LowPower. Install it from the Library Manager (arduino-cli lib install "STM32duino Low Power" "STM32duino RTC"). It provides the STOP2 entry and ATTN-pin wake that cx_sleep.h uses.

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.10

After 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

ResponsibilityFunction
Notecard configurationconfigureNotecard sends hub.set with voltage-variable sync and card.transport with cell-ntn, and returns bool
Note templatesdefineTemplates registers compact templates for all three Notefiles and returns bool
Motion and GPS configconfigureMotionAndGPS sets up the Notecard accelerometer + location mode and returns bool
Env var fetchfetchEnvOverrides pulls and clamps all environment variables on every wake
Coupler debouncereadCouplerState takes a 5-sample majority vote
Shock scoringadxl345Begin, adxl345ReadG, readPeakShockG: 64-sample burst, with each I²C read validated
Alert emissionsendAlert writes a compact Note and returns bool, and the sync is coalesced into one hub.sync after all alerts
Summary emissionsendSummary sends the latest sensor readings + shock window accumulators
Location emissionsendLocationNote 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 set shock_threshold_g for a specific installation is empirical per-install calibration, using the baseline readings you observe in railcar_status.qo. shock_windows counts 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_windows is therefore the number of wakes in the summary period where the burst peak exceeded shock_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, peakG returns NAN, and neither the shock_peak_g accumulation nor shock_windows gets 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 calls requestTemperatures(), which blocks for about 750 milliseconds during the conversion, then reads the result with getTempCByIndex(0). The library returns DEVICE_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 stores NAN, which gets reported as −9999 in tank_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 digitalRead samples 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:

alertvalueBuild
impactPeak resultant G of the triggering sample burstAll
coupled1.0 (coupler closed)All
decoupled0.0 (coupler opened)All
pressure_highFitting absolute pressure (PSI) at alert timeTANK_CAR only
pressure_dropMagnitude of the absolute-pressure drop (PSI), not the current readingTANK_CAR only
tank_temp_lowDS18B20 cargo temperature (°C) that tripped the thresholdTANK_CAR only
tank_temp_highDS18B20 cargo temperature (°C) that tripped the thresholdTANK_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 tiervoutbound intervalvinbound interval
USB60 minutes120 minutes
High120 minutes240 minutes
Normal240 minutes480 minutes
Low480 minutes720 minutes
DeadPaused (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, and configureMotionAndGPS all return bool. The firmware retries hub.set and card.transport on every wake until both succeed, and it only sets state.configVersion to CONFIG_VERSION once the templates and GPS/motion config have both succeeded. If either step fails, configVersion stays 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 requestAndResponse call checks for a NULL return and looks at the err field in the response JSON before treating the response as valid. Any non-NULL response is always paired with notecard.deleteResponse. sendRequest calls that return a bool get checked, and failures are logged.
  • Note emission error visibility. sendAlert and sendSummary use requestAndResponse (not fire-and-forget sendRequest), so the Notecard's err field is visible when something fails, and dropped Notes get logged to serial.
  • PRODUCT_UID runtime guard. If PRODUCT_UID is 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() returns false when the Notecard doesn't accept the card.attn request or ATTN never goes low (the ATTN→D9 jumper is missing). goToSleep() then logs a warning and waits out the sample interval awake with delay(), which keeps the sample cadence, and the next cycle tries to sleep again.
  • Pressure drop validity. A separate lastPressureValid flag in AppState tracks whether the stored lastPressurePsi came from a successful sensor read, and the flag clears whenever a read returns NAN. A pressure_drop alert 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 += sampleMin on each wake), not as a wake count multiplied by the current interval, so a runtime change to sample_interval_min doesn't retroactively shift the window boundary.
  • MPRLS / DS18B20 fault sentinels. Reads that fail initialization return NAN, and sendSummary replaces NAN with -9999 in pressure_psi and tank_temp_c so downstream analytics can tell a sensor fault from a legitimate near-zero reading. The DS18B20 returns DEVICE_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 to NAN before calling sendSummary. If the ADXL345 is missing or every read in a burst fails, peakG is NAN, and neither the shock_peak_g accumulation nor shock_windows gets incremented for that cycle.
  • Alert sync coalescing. sendAlert and sendLocationNote (on motion-state edges) don't set sync:true on individual note.add calls. Instead, once all the alert and location logic for a wake has run, the firmware issues a single hub.sync if 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 fetchEnvOverrides is 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

Data flow: sensors sampled every 15 min → three standard + four TANK_CAR edge rules → railcar_alert.qo (hub.sync, immediate), railcar_status.qo (every 4 h, compact template), railcar_location.qo (motion edges + every 30 min) → Notehub → routes

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 per report_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 the report_interval_min cadence, and the Notecard delivers queued Notes on the next outbound sync session. The voltage-variable hub.set schedules 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 contains coupled, moving, shock_peak_g, and shock_windows, and TANK_CAR builds also include pressure_psi and tank_temp_c. The Notecard injects the GPS coordinates automatically from its most recent fix through the _lat/_lon/_ltime compact template fields.
  • railcar_alert.qo: sent on any threshold trip. Once all the alerts for a wake are queued, the firmware issues a single hub.sync to 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's sample_interval_min wake cadence. A transition that happens between wakes gets detected and reported on the next wake, up to sample_interval_min minutes later. Once it's detected, the firmware requests a hub.sync right away in the same wake, so the yard-arrival or yard-departure Note reaches Notehub without waiting for the next scheduled outbound window. (2) Every location_interval_min minutes 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 only moving and coupled, and the Notecard injects _lat/_lon/_ltime automatically 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):

AlertConditionRepeat behaviorBuild
impactPeak G ≥ shock_threshold_gRate-limited to one alert per shock_cooldown_min window, which prevents alert storms on rough trackAll
coupled / decoupledCoupler-state edge from previous sampleEdge-detected, so one alert per state transition no matter how long the state lastsAll
pressure_highFitting absolute pressure > pressure_max_psiEdge-detected. Fires once when the condition first appears and stays quiet while it persistsTANK_CAR only
pressure_dropFitting pressure fell > pressure_drop_psi since the previous sampleOne 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 cycleTANK_CAR only
tank_temp_low / tank_temp_highDS18B20 cargo temperature outside tank_temp_min_c / tank_temp_max_cEdge-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 rangeTANK_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.

PhaseReference figureSourceWhole-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/an/a~30–40 mA
Cellular sync, average currentModem active: ~250 mANOTE-NBGLWX datasheet~150–300 mA average, with a typical session of ~15–60 s
Cellular sync, burst peakUp to ~2 A for a few ms (GSM fallback regions)NOTE-NBGLWX datasheetThe 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 currentNo NTN-specific figure is published for the NOTE-NBGLWX. Modem-active baseline: ~250 mANOTE-NBGLWX datasheetBench 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 peakNot published for NOTE-NBGLWX NTN operationn/aSize 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):

ScenarioDaily consumptionNotes
All-cellular, mostly-stationary (car in yard)15–25 mAh / 24 hGNSS 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 hGNSS 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 NTN80–200+ mAh / 24 hNTN 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→D9 jumper on the Notecarrier CX header (the firmware logs [warn] ATTN sleep failed when ATTN never 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

SymptomDiagnosisRecovery
Device does not appear in Notehub Devices tab after 5 minutesPRODUCT_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 queuedPRODUCT_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+ minutesThe 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 alwaysThe 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
    • 5.1 Safety Considerations
  • 6. Notehub Setup
    • What you should see in Notehub
  • 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
    • Simplified for this 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