Off-Grid Livestock Water Tank Monitor

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/livestock_water_tank_monitor/ so it can learn how the device works and help you adapt it to your hardware.
This project is a solar-powered remote monitoring system for off-grid livestock water tanks. It tells a rancher when a stock tank is going dry, so nobody has to drive the pasture roads to check. The device measures the water level, the pump's current draw, and the voltage of its own solar battery. It reports all three to a phone or dispatch system over cellular wherever a tower is in reach, and over satellite through Skylo's non-terrestrial network (NTN) where one isn't. The radio only comes on for an immediate alert or a 4-hour summary, so the device can run indefinitely on a modest solar panel and a single battery.
What You'll Build
By the end of this guide, you'll have a solar-powered off-grid tank monitor that:
- Samples tank level, pump current, and battery voltage every 15 minutes through analog sensors wired to a Notecarrier CX
- Alerts the rancher immediately when the tank drops below 20% full (alert) or 10% full (critical), through the Blues Notehub cloud service and its routes to SMS, push, or a webhook
- Reports system health (battery voltage and pump current) every 4 hours to a time-series database for trend analysis
- Falls back to satellite (Skylo NTN) when cellular isn't available, with the same firmware handling both transports
- Runs for weeks on solar, even through cloudy stretches, because an adaptive sleep strategy lengthens the sample interval when the battery is low
Expected data use is ~6 KB/month against the included 500 MB prepaid Blues data plan. That's the cellular path; satellite data is separate. The first production event shows up in Notehub within 4 hours of power-on.
1. Project Overview
The problem. Keeping stock tanks full in remote pastures is one of the oldest problems in agriculture, and it hasn't gone away. A rancher whose spread covers several pastures has a poly or galvanized stock tank in each one, fed by a submersible pump pulling from a well. The only way to know every tank is full is to drive every road and look. On a working ranch with pastures spread over thousands of acres, that loop can take several hours and still miss a tank that empties between visits. That's more than an inconvenience. Cattle without water for even a few hours in summer heat go downhill fast, and emergency water delivery is expensive before you even count the animals you lose.
The ways a tank fails are simple, and they repeat. A float valve sticks or fails and the tank drains down. The well drops below the pump intake, so the pump keeps running but moves no water. Or the pump motor dies and nothing moves even when the float calls for water. You don't need sophisticated modeling to catch any of these. They're observable conditions that nobody happens to be watching. The sensors here measure the same two things a rancher or hired hand would check in person: how high the water is, and whether the pump is drawing current. The third measurement, solar battery voltage, tells you whether the monitor itself is healthy and likely to keep reporting through a run of cloudy days.
Why Notecard. Stock tanks sit miles from the ranch house, well past the range of LoRa or WiFi and often past the reach of any terrestrial infrastructure. Notecard for Skylo solves both halves of the connectivity problem in one module. It uses cellular (LTE-M/NB-IoT) where a tower is in reach and falls back to the Skylo NTN satellite network where terrestrial coverage ends. Plenty of ranch pastures have mixed coverage, with a cellular signal across most of the property and dead spots in the valleys and remote corners. A tank in a valley that has coverage reports over cellular, a tank on a ridge beyond any tower reports over satellite, and the rancher sees both without needing to know which network carried the data. There's no SIM to activate, no carrier contract, and no per-site configuration. Notecard also takes care of the low-power side. In periodic mode with a 15-minute sampling interval, the radio is on for tens of seconds every few hours and off the rest of the time. That's what lets this device run indefinitely on a modest solar panel and a single battery, even through the overcast weeks of a northern winter.
New to Blues?
Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.
Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.
Deployment scenario. A weatherproof IP65 enclosure mounts on the tank post or a fence rail next to the tank opening. From above, a MaxBotix ultrasonic level sensor peers down through the lid into the tank. A clamp-on current transformer clips around one conductor of the pump's supply lead, so you don't cut any wires, and a two-resistor voltage divider reads the 12V solar battery bus. The small solar panel, charge controller, and sealed battery live in or near the enclosure. Once it's installed and calibrated, the system sends tank levels and pump health to the rancher's phone through Notehub, and nobody has to visit the site unless something goes wrong.
2. System Architecture
Device-side responsibilities. Every 15 minutes, the Notecarrier CX's
onboard STM32L433 host wakes up and reads three sensors (ultrasonic level, pump
RMS current, and solar battery voltage). It checks the alert thresholds, adds
the readings to a rolling window for the next summary, and goes back to sleep.
Between wakes the host sits in STM32 STOP2 (a few µA) until the Notecard's
ATTN pin wakes it through
card.attn. The host
talks to the Notecard entirely over I²C, so you never deal with AT commands,
serial buffers, or hand-rolled JSON.
Notecard responsibilities. Notecard for Skylo keeps
Notes in its on-device queue and picks the best
available transport, cellular or Skylo satellite. It runs sessions on the
hub.set outbound
cadence you configure (4 hours by default) and sends sync:true alert Notes
right away, whatever that cadence says. The host firmware never sees which
transport gets picked, because the same note.add JSON queues a Note for
cellular or for satellite. On each inbound sync, the Notecard also pulls
environment
variables
down from Notehub. That means an operator can tune alert thresholds, tank
calibration values, and sampling intervals from the Notehub console without
re-flashing the device. Over satellite, Notehub only delivers the variables
named in the Notecard's env.template, so the firmware registers one at boot
that lists every variable in §6.
Notehub responsibilities. Notehub receives and stores every event, whether it came in over cellular or satellite, and applies your project-level routes. Periodic summaries and alerts land in separate Notefiles, so routes can send them to different places. Alerts can go to an SMS gateway or push-notification service, and summaries to a long-term time-series store for trend analysis. Smart Fleets can sort devices automatically by pasture name or tank ID, which lets fleet-level environment variables hold site-specific calibration values without any firmware changes.
Routing to the cloud (high level only). Notehub can route to HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations. Route setup depends on your project and is outside the scope of this reference design, which doesn't ship a downstream endpoint. The Notehub routing docs cover the details.
3. Technical Summary
- Assemble hardware. Order the parts in the §4 BOM, build the circuit from §5, and mount the enclosure on the tank post.
- Flash firmware. Clone this repo, put your Notehub ProductUID in the
sketch, jumper the Notecarrier CX's
ATTNpin toD5(see §5), then build and flash:Thearduino-cli core install STMicroelectronics:stm32 arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC" arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none \ firmware/livestock_water_tank_monitor/livestock_water_tank_monitor.ino arduino-cli upload -p /dev/ttyACM0 --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none \ firmware/livestock_water_tank_monitorusb=noneboard option turns off the USB CDC stack, which has to be off for the host to sleep (see §7). These board options matchfirmware/livestock_water_tank_monitor/sketch.yaml, so you can also leave out--fqbnwhen you run the commands from the sketch directory. - Set calibration. Power on and wait for the device to sync to Notehub.
The first cellular connection happens within minutes if there's coverage,
or the first satellite connection within ~10 minutes if there isn't. Once a
tank_status.qoNote shows up in Notehub, measure your actual tank geometry and settank_depth_mmandsensor_min_mmas environment variables in Notehub (§6 step 4 has the path). After the next sync,level_pctreports accurate percentages. - Set routes. In Notehub, add two routes, one for
tank_alert.qo(to an SMS or push service) and one fortank_status.qo(to your time-series database or data lake). See §6 and the Notehub routing docs.
The later sections walk through full assembly and calibration. This quickstart gets you to a first event in under an hour.
Here is a sample Note this device emits:
{
"file": "tank_status.qo",
"body": {
"_time": 1717200000,
"level_pct": 68.4,
"distance_mm": 542.0,
"pump_amps": 7.2,
"pump_on": true,
"battery_v": 12.8,
"alerts": 0
}
}4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an embedded STM32L433 host, so this all-analog sensor mix doesn't need a separate MCU. The host sleeps in STOP2 between samples, and the Notecard's ATTN pin wakes it. |
| Notecard for Skylo (NOTE-NBGLWX) (datasheet) | 1 | Cellular (LTE-M/NB-IoT/GPRS) where a tower is in reach, and Skylo NTN satellite where it isn't. You get one module and one prepaid plan, with no SIM activation or carrier contract. It picks the transport automatically, and the host firmware never has to know which one it used. |
| Blues Mojo | 1 | Bench-only commissioning tool. Splice it inline during validation to confirm the sleep/wake duty cycle. The deployed firmware doesn't read it, and no power telemetry shows up in transmitted Notes. Remove it before field deployment. |
| MaxBotix HRXL-MaxSonar-WRMT (MB7389) | 1 | IP67-rated weatherproof ultrasonic sensor with a 300–5000 mm range, 5 mm resolution on the analog output, and a 2.7–5.5 V supply. Its analog voltage output (V_cc/5120 per mm) needs no microcontroller timing, so a single ADC pin is the whole interface. Internal temperature compensation keeps readings accurate through the wide ambient swings of an outdoor stock tank installation. |
| YHDC SCT-013-030 split-core CT, 30 A / 1 V voltage-output | 1 | Clamp-on CT that measures pump RMS current without breaking into or modifying the supply circuit. The 30 A range covers the ½–2 HP submersible pumps common in agricultural water systems. This design needs the voltage-output variant (30 A:1 V, built-in burden resistor). The current-output variant has no built-in burden and will damage the ADC pin without an external resistor. SparkFun's SEN-11005 is one of those current-output CTs (ECS1030-L72, 30 A:15 mA), so it isn't a substitute. |
| SparkFun TRRS 3.5mm Jack Breakout (BOB-11570) | 1 | Breaks the CT's 3.5mm three-conductor plug out to 0.1″ header pins so you can land the wires. |
| 10 kΩ 1% resistor (×2) | 2 | Bias divider for the CT circuit. It sets a voltage midpoint at Vref/2 so the host's ADC only sees positive voltages from the CT's AC output. |
| 10 µF electrolytic capacitor | 1 | Decouples the bias circuit. It sits in parallel with the low-side bias resistor to hold the midpoint steady. |
| BSS84 P-channel MOSFET, SOT-23 (or SI2301 or an equivalent signal-level PMOS, both easy to find at DigiKey and Mouser) | 1 | High-side switch for the battery-voltage divider. A 100 kΩ gate pullup and an MMBT3904 level shifter control it, so the divider is only active while the firmware samples A2. That removes the divider's quiescent draw between samples and keeps A2 from back-powering the MCU through its input-protection diode whenever the host is unpowered. |
| MMBT3904 NPN BJT, SOT-23 (or through-hole 2N3904 equivalent) | 1 | Shifts the 3.3 V signal from host GPIO A3 up to drive the BSS84 gate, which is referenced to the 12 V supply rail. |
| 47 kΩ resistor, 1% | 1 | High side of the battery-voltage divider, from the BSS84 drain to the divider node. When the PMOS is on, it scales the 12 V solar bus down to the 3.3 V ADC range. |
| 10 kΩ resistor, 1% (×2) | 2 | One is the low side of the battery-voltage divider (×1), and the other is the base-drive resistor from host pin A3 to the MMBT3904 (×1). |
| 100 kΩ resistor, ¼ W | 1 | BSS84 gate pullup to 12 V battery+. It keeps the PMOS switch off by default when the host MCU is unpowered, which holds A2 at GND through the low-side 10 kΩ. |
| 5V/1A step-down DC-DC regulator (e.g., Pololu D24V10F5) | 1 | Makes a stable 5V from the 12V solar battery bus. Its 5.1–36V input range covers both a healthy battery and a partly charged one without dropout. |
| 12V sealed lead-acid (SLA) or LiFePO₄ battery, ≥20 Ah | 1 | Power reserve in the field. LiFePO₄ is the better pick for its wider operating temperature range and its tolerance for deeper discharge through cold winters. Size it for the run time you expect between solar recharges. |
| Renogy Wanderer 10A PWM Solar Charge Controller | 1 | Handles panel-to-battery charging and protects against over-discharge with a low-voltage disconnect on the LOAD terminals. Any standard 12V PWM or MPPT controller with dedicated LOAD output terminals will work, because what the power chain in §5 needs is that LOAD-output isolation. |
| Renogy 20W 12V Monocrystalline Solar Panel | 1 | Sized for this design's energy budget at most US latitudes. Go up in wattage for far-northern deployments or long overcast seasons. |
Skylo-certified LTE-capable antenna. Use the one included with the NOTE-NBGLWX kit, a Blues-qualified dual-band antenna that covers the LTE bands used for cellular plus the S-Band/L-Band frequencies on B23/B255/B256 used for Skylo NTN. One antenna handles both cellular and satellite traffic, because the NOTE-NBGLWX has a single MAIN u.FL antenna port for both transports. Notecard for Skylo is certified on Skylo's network only with the antenna provided in the kit. If you replace or modify it, the device is no longer certified and Skylo may block it. Using a different antenna requires a Skylo delta-certification test. See the Satellite Best Practices guide, and contact Blues for recommended test houses. | 1 | Needs a clear, unobstructed view of the equator-facing sky for the satellite path (the southern sky in the northern hemisphere, the northern sky in the southern hemisphere). Mount it flat on the enclosure lid or on a raised bracket above the enclosure, and never inside a metal enclosure. |
| u.FL to SMA female pigtail, 100 mm+ (e.g. SparkFun WRL-09145 or equivalent RG316/RG178 assembly) + IP67-rated SMA bulkhead fitting | 1 each | Carries the included Skylo-certified antenna's connection from the NOTE-NBGLWX MAIN u.FL port through the enclosure wall. Match the bulkhead hole size to your enclosure. DigiKey and Mouser both stock nickel-plated and stainless SMA bulkheads. Use the shortest pigtail that reaches the bulkhead to keep cable loss down. |
| IP65+ weatherproof enclosure, ≥8×6×3″ | 1 | Holds the electronics. Mount it on the tank post or a nearby fence rail with stainless hardware. |
All Blues hardware ships with an active SIM that includes 500 MB of cellular data and 10 years of service, with no activation fees and no monthly commitment. The NOTE-NBGLWX service plan also includes Skylo satellite data. The Blues pricing page lists the current satellite data allotments and usage terms.
5. Wiring and Assembly
All host I/O lands on the Notecarrier
CX dual 16-pin header,
and Notecard for Skylo seats in the M.2 slot. During commissioning, the Mojo
gets spliced inline between the step-down regulator output and the
Notecarrier's +VBAT pad to measure power on the bench. The deployed firmware
never reads it, and it comes out before field installation.
Safety. This installation involves the pump's supply conductors, a 12V battery, solar panel wiring, and charge-controller terminals, and any of them can carry hazardous voltages or stored energy. De-energize and lock out / tag out the pump circuit before installing or modifying any wiring. Follow the pump and charge-controller manufacturers' installation instructions in full. Put a fuse in the positive battery lead as close to the battery terminal as you practically can. Use 3–5 A for the monitor load, and check the charge-controller manual for the panel and battery leads. Electrical work should be done by qualified personnel following applicable local electrical codes.
Solar power chain (panel, charge controller, battery):
- Solar panel + → charge controller PV + input, and solar panel − → charge controller PV − input.
- Charge controller BATT + → 12V battery positive terminal, and charge controller BATT − → 12V battery negative terminal. If the controller doesn't include a fuse, put a 3–5A automotive blade fuse in the positive wire between the battery terminal and the controller's BATT + terminal.
- Take the monitor load from the charge controller's LOAD output terminals, not straight from the battery terminals. This lets the controller's low-voltage disconnect protect the battery from deep discharge during long overcast periods.
- Common ground: charge controller BATT −, the LOAD − output, and the 12V battery negative terminal must share a common ground bus. Tie them together with a short, low-resistance wire to get rid of ground-offset measurement errors on the A2 battery-voltage divider.
Monitor power chain (charge controller LOAD → Notecarrier):
Charge controller LOAD + → Pololu D24V10F5 step-down input VIN (5V
output) → Notecarrier CX +VBAT. The Notecarrier draws from +VBAT and
generates the 3.3V rail for the host and sensors internally. For bench
commissioning, splice the Mojo inline between the step-down output and +VBAT
to measure power, then take it out before field deployment. The top of the
battery-voltage divider connects straight to the battery terminals (not the
LOAD terminals) so it reads the true battery voltage. The voltage-divider
circuit below covers the PMOS high-side switch that controls when current flows
through the resistor chain.
Level sensor (A0):
- MaxBotix MB7389
V+→ Notecarrier+3V3_OUT - MaxBotix MB7389
GND→ NotecarrierGND - MaxBotix MB7389
AN(analog voltage output pin) → NotecarrierA0 - Leave the MB7389
TX(TTL serial) andPW(pulse-width) pins unconnected, since this design only usesAN.
Pump current sensor (A1): Clamp the SCT-013-030 jaws around one conductor of the pump's supply cable. Either leg works, but clamping both cancels out and reads zero. Then wire the bias circuit:
- Notecarrier
+3V3_OUT→ 10 kΩ resistor → bias node → 10 kΩ resistor → NotecarrierGND. These two resistors form a symmetric voltage divider, so the bias node sits at about 1.65 V (Vcc/2). - SCT TRRS breakout
SLEEVE→ bias node (this lifts the CT's AC output onto the DC bias midpoint) - SCT TRRS breakout
TIP→ NotecarrierA1 - Bias node → 10 µF electrolytic capacitor positive lead, and capacitor
negative lead →
GND(the cap holds the DC midpoint steady during pump-start transients)
Don't connect either CT lead to GND. The CT's built-in burden is only about
60 Ω, so a grounded lead pulls the bias node down to about 20 mV and clips the
negative half of every AC cycle.
Solar battery voltage divider (A2 + A3): A BSS84 P-channel MOSFET works as a high-side switch between the 12 V battery bus and the top of the voltage-divider resistor chain. Without it, the divider would draw from the battery bus all the time. A2 would also stay driven whenever the host MCU is unpowered, which can back-power the host through the A2 input-protection diode. A 100 kΩ gate pullup (battery+ → gate) keeps the PMOS off by default. When the firmware needs a battery sample, an MMBT3904 NPN level-shifts the 3.3 V host GPIO (A3) to override that pullup and turn the PMOS on.
Wire the circuit like this:
- 12 V battery positive → BSS84 source
- BSS84 drain → 47 kΩ resistor → divider node → 10 kΩ resistor → Notecarrier
GND - Divider node → Notecarrier
A2 - BSS84 gate → 100 kΩ resistor → 12 V battery positive (gate pullup)
- BSS84 gate → MMBT3904 collector
- MMBT3904 emitter → Notecarrier
GND - MMBT3904 base → 10 kΩ resistor → Notecarrier
A3
Operation. Before sampling, the firmware drives A3 HIGH. The MMBT3904 saturates and pulls the BSS84 gate close to GND (V_GS ≈ −12 V, well past the −0.8 to −2 V threshold). After the four ADC samples, the firmware drives A3 LOW. The MMBT3904 turns off, and the 100 kΩ pullup brings the gate back to 12 V (V_GS = 0 V), which turns the PMOS off within ≈ 100 µs. A3 stays LOW while the host sleeps in STOP2, because STOP2 retains GPIO state. If the host is ever unpowered and A3 floats, the 100 kΩ pullup holds the PMOS gate at 12 V on its own. Either way, A2 sits at GND through the low-side 10 kΩ, and no leakage path reaches the A2 protection structures.
The switch leaves the voltage scaling unchanged: battery_V = V_adc / 0.175,
which maps 0–14.5 V to 0–2.54 V, inside the 3.3 V ADC range. Because the
divider connects to the battery terminals (not the LOAD terminals), the MCU
reads the true battery bus voltage.
ATTN-to-D5 jumper (host sleep):
Fit a short jumper between the ATTN and D5 pins on the Notecarrier CX.
Both are on the same 16-pin header, with ATTN at position 3 and D5 at
position 14. The Notecard pulls ATTN low while the host sleeps and raises it
when the next sample is due, and the rising edge on D5 wakes the host from
STOP2. Leave the Notecarrier CX EN pin unconnected. On the CX it enables the
shared 3.3 V VIO rail (host, Qwiic, 3V3_OUT, and Notecard I/O), so driving
it from ATTN would brown out the whole board instead of putting the host to
sleep. Putting a Host to Sleep Between Sensor
Readings
covers the full pattern.
Antennas: The NOTE-NBGLWX has two u.FL ports. MAIN carries both cellular
and satellite, served by a single Skylo-certified LTE-capable antenna that
covers LTE and S-Band/L-Band frequencies. GPS is for a passive GPS/GNSS
antenna, which this firmware doesn't use, so you can leave it unconnected. Run
a short u.FL-to-SMA female pigtail (≥100 mm, RG316/RG178) from the MAIN u.FL
port to an SMA female bulkhead in the enclosure wall, then connect the included
Skylo-certified antenna to the bulkhead from outside with its SMA connector.
Use the antenna that comes in the kit. Swapping in a third-party cellular
antenna voids the Skylo certification, and Skylo may block the device from its
network. Mount the antenna on the outside of the enclosure lid or on a raised
bracket with an unobstructed view of the equator-facing sky (the southern sky
in the northern hemisphere, the northern sky in the southern hemisphere). An
antenna inside a metal enclosure won't acquire Skylo GEO satellites, and
neither will one that trees, structures, or terrain block toward the equator.
Plan for sky view even where cellular reception is good enough, so the
satellite fallback stays available. The Satellite Best Practices
guide has more siting guidance.
Level sensor mounting: Mount the MB7389 vertically, with the sensing face
pointing straight down, through a 1.5–2 inch hole in a tank lid or on a bracket
over the open top. Keep at least 300mm between the sensor face and the highest
water surface you expect. That's the MB7389's blanking zone, and readings inside
it aren't reliable. After installation, record the actual mounting height above
the full waterline and enter it as sensor_min_mm in Notehub.
6. Notehub Setup
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID and paste it into
firmware/livestock_water_tank_monitor/livestock_water_tank_monitor.inoasPRODUCT_UID. -
Claim the Notecard. Power up the unit. The Notecard associates itself with your project on its first successful Notehub session, whether that's over cellular or Skylo satellite.
-
Create a Fleet per site. Fleets group devices for shared configuration and routing. One fleet per pasture is a natural fit here, since the tanks in a pasture share roughly the same tank geometry, pump type, and battery system. Smart Fleets can sort devices automatically by a tag or environment variable, so you don't have to assign them by hand as the fleet grows.
-
Set environment variables. Every variable is optional. If one isn't set in Notehub, the firmware uses its compile-time default. Changes reach the device on the next inbound session, with no re-flashing. The two that matter most are the tank calibration values (
tank_depth_mmandsensor_min_mm). You have to set them for each installation to get accurate level percentages.Variable Default Purpose tank_depth_mm1200Distance (mm) from the sensor face to the dry tank bottom. Measure it with the tank empty. sensor_min_mm300Distance (mm) from the sensor face to the water surface when the tank is full. Must be ≥ 300 mm, the MB7389 blanking-zone minimum. Any closer and the sensor can't produce valid readings. The compile-time default equals the blanking-zone floor and is only a placeholder, so replace it with the measured full-tank distance during commissioning. level_alert_pct20Tank level (%) below which a level_lowalert fires. The rancher still has time to act.level_critical_pct10Tank level (%) below which a level_criticalalert fires. Cattle could start to dehydrate within hours.pump_on_amps1.0RMS amps above which the pump counts as running. Set it above the CT noise floor and below the pump's normal running current. battery_alert_v11.5Solar battery voltage (V) below which a battery_lowalert fires. Points to a long stretch of overcast weather or a charge controller fault.sample_interval_sec900Seconds between sensor samples (15 minutes by default). Lower it during calibration, along with summary_interval_min, so each summary window has at least one fresh sample. See §9.summary_interval_min240Minutes between periodic summary Notes. The default is 4 hours (6 Notes per day). Must be at least ⌈ sample_interval_sec÷ 60⌉ minutes, because a summary window shorter than one sample wake has no data in it. The firmware clamps lower values up to this floor at runtime, so when you need a short calibration window, lowersample_interval_secto match (see §9). When this value changes, the firmware notices on the next wake and re-issueshub.setwith the new cadence. That keeps the Notecard's outbound sync interval lined up with the local summary schedule, and nobody has to reboot the device.alert_cooldown_sec3600Minimum seconds between repeated level_lowandlevel_criticalalerts. Thebattery_lowalert is edge-triggered instead and ignores this cooldown. It fires once per episode and only re-arms after the voltage recovers abovebattery_alert_v + 0.5V. -
Configure routes. Add at least one route for
tank_alert.qo(to an SMS gateway, push-notification service, or on-call webhook) and a second fortank_status.qo(to a time-series store for trend analysis and historical review). Because the two Notefiles are separate, alerts can hit your phone right away while summaries go to a database in a nightly batch, and the route doesn't need any filtering logic.Example alert payload (immediate,
sync:true):{ "alert_code": 1, "level_pct": 8.5, "pump_amps": 0.0, "battery_v": 12.4, "_time": 1717200000 }Alert codes:
0= tank below 20% (low alert),1= tank below 10% (critical),2= battery below 11.5V.Example status payload (every 4 hours):
{ "level_pct": 68.4, "distance_mm": 542.0, "pump_amps": 7.2, "pump_on": true, "battery_v": 12.8, "alerts": 3, "_time": 1717200000 }alertsis the number oftank_alert.qoNotes sent since the last summary.pump_oncomes from comparing the window-averagepump_ampsagainst thepump_on_ampsthreshold.
7. Firmware Design
The firmware is four files in the
firmware/livestock_water_tank_monitor/
directory, and the .ino is the entry point:
| File | Role |
|---|---|
livestock_water_tank_monitor.ino | Entry point. setup() runs once to configure the Notecard, register templates, and arm the ATTN wake. loop() runs one wake cycle. It loads the env-var cache, reads the sensors, checks alerts, sends the summary if it's due, and puts the host in STOP2 until the Notecard raises ATTN. |
livestock_water_tank_monitor_helpers.h | Compile-time constants, the GlobalState struct, extern declarations for all cross-file globals, and helper function prototypes. |
livestock_water_tank_monitor_helpers.cpp | Implementations of the sensor-read, env-var parsing, alert, and summary helpers. |
cx_sleep.h | Host sleep: card.attn request, STOP2 entry, ATTN wake. |
Dependencies:
- Arduino core for STM32
(
stm32duino/Arduino_Core_STM32). Blues Wireless Notecard, thenote-arduinolibrary. Install it from the Arduino Library Manager or download it from the note-arduino releases.STM32duino Low Powerand its dependencySTM32duino RTC, both from the Library Manager. Low Power provides the STOP2 entry and ATTN-pin wake used incx_sleep.h.
Build settings: The Notecarrier CX's host is the same STM32L433 as the Blues
Cygnet, so select Blues boards → Cygnet (pnum=CYGNET) and set USB
support (if available) to None (usb=none). If the USB CDC stack is
enabled and no USB host is attached (battery power, or the Notecarrier CX USB-C
switch in NC), the STM32's USB wakeup interrupt fires continuously and the
host never stays in STOP2. Debug output (when TANK_MONITOR_DEBUG is defined)
goes to the LPUART on the Notecarrier CX debug jack instead, which an ST-LINK V3
exposes as a virtual COM port (see Serial Logging With
STLINK).
Modules
| Responsibility | Function |
|---|---|
Notecard configuration (hub.set, card.transport, env.template, card.motion.mode) | notecardConfigure |
Re-issue hub.set when summary_interval_min env var changes | reapplyHubSet |
| Template registration for both Notefiles | defineTemplates |
| Environment-variable fetch (each wake) | fetchEnvOverrides |
| Ultrasonic raw distance reading | readDistanceMm |
| Distance-to-level-percent conversion | readLevelPct |
| Pump RMS current | readPumpAmps |
| Solar battery voltage | readBatteryV |
| Alert evaluation and emission | evaluateAlerts, sendAlert |
| Periodic summary emission | sendSummary |
Host sleep (STOP2 until the Notecard raises ATTN) | cx_sleep.h: cxSleepBegin, cxSleepUntilAttn |
| Battery-adaptive sleep duration | doSleep |
| Application state (held in RAM across sleep cycles) | GlobalState |
Sensor reading strategy
Level sensor. The MB7389 puts out an analog voltage proportional to the
distance it measures, all the time: V_out = V_cc / 5120 × distance_mm. At 3.3V,
the output maps to 0–3.22V across the 300–5000mm range, which fits comfortably
inside the host's 3.3V ADC reference. The firmware averages 16 samples of the
12-bit ADC counts to knock down noise, converts the result to millimeters, and
then maps it to a 0–100% fill level using the calibrated tank_depth_mm
(empty) and sensor_min_mm (full) values. Any reading outside the MB7389's
documented 300–5000mm range gets flagged invalid and left out of the
accumulator. The firmware accumulates both distance_mm and level_pct across
every valid sample in the summary window and reports them as window averages in
the periodic summary Note.
Pump current. The SCT-013-030 CT produces an AC voltage centered on the bias midpoint (~1.65V). The firmware first measures the DC bias as a 256-sample mean. It then collects ~1500 samples, which covers about 20 AC mains cycles at the STM32's ADC throughput, and computes the root-mean-square deviation from that bias. The RMS voltage converts to current at 30A per volt. Anything below a 0.15A noise floor gets clamped to zero so ADC noise doesn't trigger false "pump running" detections.
Battery voltage. The firmware averages 4 samples from the A2 ADC channel
and scales the result back through the voltage divider ratio:
battery_V = (adc × 3.3 / 4095) / (10 / 57). That's the actual 12V solar
battery bus voltage, not the regulated 5V rail, so it gives you a real read on
the system's energy state.
Event payload design
Both Notefiles are
template-backed,
so the Notecard stores their Notes as fixed-length binary records instead of
free-form JSON. Compact binary encoding cuts per-Note wire size by roughly
3–5×. More importantly, the NOTE-NBGLWX Skylo NTN satellite path requires it.
Both templates are registered with "format": "compact" and an explicit port
number, so either Notefile can go out over satellite or cellular with the same
note.add call. Over a multi-year deployment at 6 summary Notes per day, that
compression also stretches the 500 MB prepaid data budget a good deal further.
Alert Notes carry sync:true to ask for an immediate radio wake. Templated
Notes normally drop zero-valued fields when Notehub renders them, so both
note.add calls pass full:true. That way pump_amps = 0.0 (pump off),
level_pct = 0.0 (empty tank), pump_on: false, and alerts: 0 all arrive
exactly as sent.
tank_status.qo (periodic, every summary_interval_min, compact
template-encoded, port 50):
{
"file": "tank_status.qo",
"body": {
"_time": 1717200000,
"level_pct": 68.4,
"distance_mm": 542.0,
"pump_amps": 7.2,
"pump_on": true,
"battery_v": 12.8,
"alerts": 0
}
}pump_on is a derived field, not a per-sample boolean. Each sample adds
its RMS current reading to a running average. At summary time, sendSummary
gets that 4-hour window average as pumpAmps and sets
pump_on = (pumpAmps >= pump_on_amps). So a value of true means the average
pump current over the summary window was at or above the running threshold.
It doesn't mean the pump ran continuously, or that it ran for any particular
fraction of the window. A pump that ran briefly at the start of a 4-hour window
and then stopped can produce a non-zero pump_amps average with
pump_on: false, while a pump that ran all window long will produce
pump_on: true. Use pump_amps (the numeric average) for trend analysis, and
treat pump_on as a coarse classification of the summary window, where true
only says the window-average current cleared the threshold.
tank_alert.qo (immediate, sync:true, compact template-encoded, port 51):
{
"file": "tank_alert.qo",
"body": {
"_time": 1717200000,
"alert_code": 0,
"level_pct": 18.1,
"pump_amps": 0.0,
"battery_v": 12.6
},
"sync": true
}alert_code is an integer type code: 0 = level_low, 1 =
level_critical, 2 = battery_low. An integer keeps the compact-template
payload fixed-length binary, which the NOTE-NBGLWX Skylo NTN path requires
here just as it does for the summary template. Because sendAlert() sets
full:true, all five fields are always present, including alert_code: 0 and
pump_amps = 0.0 when the pump is off.
Low-power strategy
The host is only awake for a few seconds of each 15-minute sample cycle. After
each wake, doSleep() calls cxSleepUntilAttn() (in cx_sleep.h). That sends
a card.attn sleep
request for the computed interval, waits for the Notecard to pull ATTN low,
and puts the STM32L433 into STOP2, where it draws a few microamps and keeps RAM
intact. When the interval runs out, the Notecard raises ATTN. The rising edge
on D5 wakes the host, and execution picks up on the next line of loop().
Because the host resumes in place instead of cold-booting, the runtime state
just lives in the GlobalState struct in RAM. That covers the accumulator
values, alert cooldown timestamps, summary window anchor, last-applied hub
cadence, and the env-var cache. Nothing gets serialized to the Notecard, and
setup() runs only once per power-up. If the sleep request fails or ATTN
never goes low (because the jumper is missing, say), loop() waits out the
interval awake and tries again on the next cycle, so the sample cadence holds
either way.
The Notecarrier CX's +3V3_OUT rail stays up while the host sleeps, so the
MB7389 level sensor and the CT bias network stay powered between samples. The
MB7389 keeps ranging at a few milliamps, and that sets the practical sleep floor
for this design. For a battery-constrained site, the obvious next step is to
gate its V+ from a spare GPIO. The battery-voltage measurement circuit, on
the other hand, is off between samples. A3 holds LOW through STOP2 and the
100 kΩ pullup keeps the BSS84 PMOS gate at 12 V, which leaves A2 at GND through
the low-side 10 kΩ and draws zero quiescent current from the battery bus. The
NOTE-NBGLWX Notecard itself idles at approximately 18 µA @ 5V in its own
low-power idle
state between
sessions, and the Skylo satellite modem sits in low-power standby when it isn't
transmitting.
Sampling and transmission are separate on purpose. The device samples every 15
minutes but only sends summaries every 4 hours. Alerts skip the transmit timer
with sync:true, so a critically low tank reaches the rancher's phone as soon
as a session comes up, no matter where the Notecard is in its outbound cycle.
Over cellular, that usually takes 15–60 seconds. The Skylo NTN satellite link
is slower to connect, because the Notecard has to acquire the GEO satellite and
finish NTN registration before any data moves. Depending on satellite geometry
and signal conditions, that can take a few minutes. Alert Notes still go out
over satellite when cellular isn't available. They arrive later than they would
over cellular, but on an off-grid tank with no other way to reach anyone, a
missed alert is worse than a slightly late one. At sites with reliable
cellular, the Notecard uses cellular first and never touches the satellite
path, so the satellite energy cost doesn't apply.
When battery voltage drops below 12.0V (moderately discharged), the firmware
doubles the sleep interval to 30 minutes. Below battery_alert_v (11.5V by
default, deeply discharged), it quadruples the interval to 60 minutes and fires
a battery_low alert the first time the voltage crosses that threshold. While
the battery stays low, further battery_low alerts are suppressed, and they
re-arm only after the voltage recovers above battery_alert_v + 0.5V. Without
that edge trigger, the 60-minute emergency-sleep cadence would open a
sync:true radio session on every wake and make the depletion it's warning
about even worse. The longer sleep intervals give the system a multi-day power
reserve before the step-down regulator drops out. You give up some sampling
granularity, and in return the monitor survives long overcast stretches.
Retry and error handling
- The firmware keeps the last
outboundvalue it sent tohub.setinGlobalState(appliedSummaryIntervalMin). After eachenv.get, ifsummary_interval_mindoesn't match the stored value,reapplyHubSet()re-issueshub.set. That keeps the cellular outbound cadence in step with the summary interval the operator set, with no physical reboot. - A single
templatesInstalledflag inGlobalStatetracks bothnote.templateregistrations (summary and alert). The flag only gets set when both templates succeed. If either registration fails on cold boot, the flag stays false andloop()retries both on every wake until they both work. Otherwise a single failed registration would leave one or both Notefiles in free-form mode, skipping the compact binary encoding the satellite path needs. - Level readings outside the MB7389's valid range (300–5000mm) stay out of the
window accumulator. If a whole 4-hour window produces no valid level
readings, the summary sends
level_pct: -1.0as a sentinel, so downstream analytics can tell a "sensor fault" apart from a real near-zero fill. - Pump current readings below the noise floor get clamped to 0.0 instead of being excluded, because a zero reading means "pump is off," not "no data."
- If
card.timereturns 0 (the Notecard hasn't synced and picked up network time yet), the alert evaluator skips threshold checks for that wake cycle. That stops spurious alerts on the first cold boot. - Alert de-duplication:
level_lowandlevel_criticalalerts are cooldown-gated (one peralert_cooldown_sec, 1 hour by default) so a slowly draining tank doesn't page the rancher over and over.battery_lowis edge-triggered. It fires once on the first crossing belowbattery_alert_vand stays quiet until the voltage recovers abovebattery_alert_v + 0.5V. That keeps the 60-minute emergency-sleep cadence from opening async:trueradio wake on every cycle while the battery is already under stress.
Key code snippet 1 — template registration
The template uses "compact" format and an explicit port, and the NOTE-NBGLWX
Skylo NTN satellite path requires both. Type hints are literal numeric values
that stand for field types:
14.1= 32-bit float (e.g.,level_pct,pump_amps,battery_v)12= 2-byte signed integer / int16_t (e.g.,alertsfield, range −32,768 to +32,767)true= boolean field (e.g.,pump_on)14= Unix timestamp field, which the Notecard fills in automatically on eachnote.add, so the host doesn't need to supply the timestamp.
Template-backed Notes cut per-event wire size by ~4×, and satellite delivery
requires them. The tank_alert.qo template that follows uses the same pattern
on port 51, and g.templatesInstalled only goes true once both are accepted.
If either one fails, defineTemplates() returns early and loop() tries again
on the next wake.
J *req = notecard.newRequest("note.template");
if (!req) return;
JAddStringToObject(req, "file", NOTEFILE_SUMMARY);
JAddNumberToObject(req, "port", 50); // required for Skylo NTN
JAddStringToObject(req, "format", "compact"); // required for Skylo NTN
J *body = JAddObjectToObject(req, "body");
if (!body) { JDelete(req); return; }
JAddNumberToObject(body, "_time", 14); // Unix timestamp (Notecard auto-fills)
JAddNumberToObject(body, "level_pct", 14.1); // float32
JAddNumberToObject(body, "distance_mm", 14.1); // float32
JAddNumberToObject(body, "pump_amps", 14.1); // float32
JAddBoolToObject(body, "pump_on", true); // boolean
JAddNumberToObject(body, "battery_v", 14.1); // float32
JAddNumberToObject(body, "alerts", 12); // int16 (2-byte signed)
if (!notecard.sendRequest(req)) return; // retry on next wake if this fails
// ...
if (notecard.sendRequest(req)) {
g.templatesInstalled = true; // set only after both templates confirmed
}Key code snippet 2 — immediate alert with sync
The alert Note uses the compact template registered for tank_alert.qo (port
51, "compact" format). That lets it travel over the NOTE-NBGLWX Skylo NTN
satellite path when cellular isn't available, and the same note.add call
works on either transport. sync:true tells the Notecard to wake the radio
right away instead of waiting for the next scheduled outbound window. The
alert_code integer (0 = level_low, 1 = level_critical, 2 = battery_low)
keeps the body fixed-length binary, as the compact template requires.
Templated Notes drop zero-valued fields by default, so full:true keeps
alert_code: 0 and pump_amps: 0.0 in the body. Until both templates are
registered, sendAlert() returns false without queuing anything, because a
free-form Note would never go out over satellite. The caller only records the
cooldown when it gets true back, so the next wake tries again.
if (!g.templatesInstalled) return false;
J *req = notecard.newRequest("note.add");
if (!req) return false;
JAddStringToObject(req, "file", NOTEFILE_ALERT);
JAddBoolToObject(req, "sync", true);
JAddBoolToObject(req, "full", true);
J *body = JAddObjectToObject(req, "body");
if (!body) { JDelete(req); return false; }
JAddNumberToObject(body, "alert_code", alertCode);
JAddNumberToObject(body, "level_pct", levelPct);
JAddNumberToObject(body, "pump_amps", pumpAmps);
JAddNumberToObject(body, "battery_v", battV);
return notecard.sendRequest(req);Key code snippet 3 — battery-adaptive sleep until ATTN
When the solar battery is low, the sleep duration gets longer, which saves
energy through multi-day overcast stretches without any cloud-side
configuration change. cxSleepUntilAttn() then asks the Notecard to hold
ATTN low for that many seconds, confirms the pin went low, and puts the STM32
into STOP2. Since the host stays alive, the firmware checks the result of the
card.attn request instead of firing it and forgetting it. setup() arms the
ATTN wake once with cxSleepBegin(), and doSleep() runs at the end of every
loop() pass. With the default sample_interval_sec of 900, that's 15
minutes on a healthy battery, 30 minutes below 12.0V, and 60 minutes below
battery_alert_v. The result is capped at ENV_SAMPLE_SEC_MAX (86,400
seconds, or 24 hours), so a long base interval can't stretch into days.
// Wake from STOP2 on the ATTN rising edge (ATTN jumpered to D5).
cxSleepBegin();
// ...
static void doSleep(float battV) {
uint32_t sleepSec = g_sampleIntervalSec;
// Moderately discharged (< 12.0V): extend sleep to reduce radio wakes.
if (battV > 0.0f && battV < 12.0f) {
sleepSec = g_sampleIntervalSec * 2;
}
// Critically low (below battery_alert_v): extend further to survive.
if (battV > 0.0f && battV < g_batteryAlertV) {
sleepSec = g_sampleIntervalSec * 4;
}
// Hard ceiling: clamp after multipliers so the device never sleeps longer
// than the documented 24-hour maximum (e.g. 86400 × 4 would be 4 days).
if (sleepSec > ENV_SAMPLE_SEC_MAX) {
sleepSec = ENV_SAMPLE_SEC_MAX;
}
// ...
if (!cxSleepUntilAttn(notecard, sleepSec, NULL, log)) {
// The Notecard didn't take the sleep request, or ATTN never went low
// (check the ATTN -> D5 jumper). Keep the sample cadence awake and
// try again on the next cycle.
// ...
delay(sleepSec * 1000UL);
}
}Key code snippet 4 — dynamic hub.set cadence
When an operator changes summary_interval_min in Notehub, the firmware picks
up the change on the next wake and calls reapplyHubSet(), so the Notecard's
outbound cellular cadence stays in sync without a physical reboot. The check
only runs when fetchEnvOverrides() got a fresh env.get response
(envOk). Once hub.set succeeds, the firmware also clears the summary
accumulators and the window start, so the next summary covers one full window
at the new cadence. If hub.set fails, the samples are kept and the next wake
tries again.
if (envOk && g_summaryIntervalMin != g.appliedSummaryIntervalMin) {
if (reapplyHubSet()) {
g.lastSummaryEpoch = 0;
g.levelPctAccum = 0.0f;
// ...
}
}
// ...
static bool reapplyHubSet(void) {
J *req = notecard.newRequest("hub.set");
if (!req) return false;
JAddStringToObject(req, "product", PRODUCT_UID);
JAddStringToObject(req, "mode", "periodic");
JAddNumberToObject(req, "outbound", (int)g_summaryIntervalMin);
JAddNumberToObject(req, "inbound", (int)g_summaryIntervalMin);
if (notecard.sendRequest(req)) {
g.appliedSummaryIntervalMin = g_summaryIntervalMin;
return true;
}
return false;
}8. Data Flow
Every 15 minutes the host wakes, reads three sensors, and checks three independent alert conditions. Valid readings build up across wakes in a rolling window, and each 4-hour summary averages every metric over that window.
Collected each sample. Tank fill level (distance mm → level percentage),
pump RMS amps, solar battery voltage (V), and per-alert cooldown timestamps.
pump_on isn't sampled directly. The firmware derives it at summary time from
the window average of pump_amps (see §7).
Transmitted.
tank_status.qo: one compact template-encoded Note (port 50) everysummary_interval_min(4 hours by default, 6 Notes per day). Each numeric field (level_pct,distance_mm,pump_amps, andbattery_v) is the mean of the valid samples collected over the window. A sensor with zero valid reads in the window reports-1.0as a sentinel, andalertscarries the number of alert Notes sent since the last summary.pump_onis a derived boolean set at summary time. It'struewhen the window-averagepump_ampsis at or abovepump_on_amps(1.0 A by default), so it reflects the window average, not a per-sample observation.tank_alert.qo: a compact template-encoded Note (port 51) sent immediately withsync:truewhenever a threshold trips. The Skylo NTN satellite path needs the compact template and explicit port, and the samenote.addgoes out over cellular or satellite depending on which transport the Notecard picks.alert_codeis an integer field (0 =level_low, 1 =level_critical, 2 =battery_low).level_lowandlevel_criticalare cooldown-gated (one peralert_cooldown_sec, 1 hour by default) so a slowly drifting tank doesn't generate hundreds of pages.battery_lowis edge-triggered. One alert fires on the first crossing belowbattery_alert_v, and no more fire until the voltage recovers abovebattery_alert_v + 0.5V.
Alert triggers.
level_low: tank fill percentage belowlevel_alert_pct(20% by default). The rancher has time to act, since at normal cattle consumption a tank at this level usually takes several hours to empty.level_critical: tank fill percentage belowlevel_critical_pct(10% by default). This one is urgent, because cattle dehydration risk is hours away, not days.battery_low: solar battery voltage belowbattery_alert_v(11.5V by default). It points to a long cloudy stretch, a faulty solar panel, or a charge controller problem, and it means the monitor itself is at risk of going offline.
Routed. Both Notefiles go to Notehub and from there to wherever the
project's routes send them. Because they're kept separate, tank_alert.qo can
go to an SMS gateway to notify the rancher right away while tank_status.qo
goes to a time-series database for historical trend analysis and predictive
maintenance, and the routes don't need any per-Note filtering logic.
9. Validation and Testing
Expected cadence in normal operation. A healthy, full tank produces one
tank_status.qo Note every 4 hours and no tank_alert.qo Notes. After the
first power-on, confirm that a summary Note shows up in Notehub within 4 hours
and that level_pct is non-zero and makes sense for how full the tank actually
is.
Tank calibration. distance_mm in tank_status.qo is a 4-hour window
average, not a point-in-time measurement. If you read it right after filling or
draining the tank, you get an average over a window that spans several water
levels, and it won't match the current surface distance. Before calibrating,
set both sample_interval_sec to 60 (1 minute) and
summary_interval_min to 5 in Notehub, then wait for the device to pick up
both changes on its next inbound sync. The sample wake cadence puts a floor
under the summary cadence. The firmware enforces
summary_interval_min ≥ ⌈sample_interval_sec ÷ 60⌉, so lowering
summary_interval_min without also lowering sample_interval_sec leaves the
effective window length where it was. With both set as above, the device wakes
every minute and sends a summary every 5 minutes. Once the tank has held a
stable level for one full 5-minute window, the averaged distance_mm comes
very close to the actual surface distance.
Fill the tank completely and hold it at its overflow point for at least one
full 5-minute window. The distance_mm value in the summary Note that follows
is your sensor_min_mm. Then drain the tank completely, to the bare
bottom, with no water left in it. Hold it empty for another full window and
note distance_mm again. That value is tank_depth_mm. If you calibrate with
the tank "nearly empty" instead of truly empty, you'll underestimate
tank_depth_mm, which squeezes the level-percentage range and makes every
reading come out high. If you can't safely drain the tank, measure the actual
residual depth in millimeters and add it to the measured distance_mm before
entering the total as tank_depth_mm. Enter both calibrated values as
device-level environment variables in Notehub, and set sample_interval_sec
and summary_interval_min back to their operational values (900 and 240
by default). After the next inbound sync, level_pct should read ~100% when
full and ~0% when empty.
Pump current verification. pump_amps in tank_status.qo is also a
4-hour window average. If the pump ran on and off during that window, comparing
a handheld clamp-meter reading against a normal summary Note will mislead you.
To get a number you can compare, first set both sample_interval_sec to 60
and summary_interval_min to 5 in Notehub, as you did for tank calibration,
and wait for the device to pick up the changes. Once the pump has run
continuously at a steady load for one full 5-minute window, put a calibrated AC
clamp meter around the same conductor the CT monitors and note the reading. It
should agree with pump_amps in the next summary Note to within a few percent.
If it's off by more than that, the CT jaws usually aren't fully closed, or the
CT is clamped around both conductors and they're canceling out. A reading near
zero with the pump running points to a wiring fault at the bias circuit
midpoint. When you're done, set sample_interval_sec and
summary_interval_min back to their operational values.
Simulating an alert. Lower level_alert_pct to 99 in Notehub. The next
inbound sync pulls the new value, the sample cycle after that fires a
level_low alert, and the alert shows up in Notehub as soon as a cellular
session comes up. Once you've confirmed it, set the threshold back to the
production value you want.
Using Mojo to validate power behavior (bench-only). In this design the Mojo is bench instrumentation only. The deployed firmware doesn't read the Mojo's LTC2959 coulomb counter at runtime, and no power telemetry shows up in transmitted Notes. Use it during commissioning to check the sleep/wake profile before field installation.
Splice the Mojo inline between the step-down regulator output and the
Notecarrier's +VBAT pad. The NOTE-NBGLWX Notecard datasheet gives
approximately 18 µA @ 5V for the Notecard's own low-power idle state. That's
the quiescent draw of the bare Notecard chip at its VMODEM_P supply, measured
on its own. A Mojo spliced at +VBAT sees the full load on that rail, which is
the Notecard plus the Notecarrier CX's own board quiescent consumption
(regulators and any always-on carrier-board circuitry). Expect the measured
idle floor to land above the bare-Notecard datasheet figure. The STM32L433 host
in STOP2 adds only ~1–2 µA, and the battery-voltage PMOS switch holds A2 at
GND. The MB7389, though, stays powered from +3V3_OUT and keeps ranging at a
few milliamps between samples, so that sensor sets the floor you'll actually
see. Don't chase the bare-Notecard datasheet number. Use the Mojo trace to
check the shape of the power profile instead: long, flat idle stretches at
the system quiescent floor, broken up by brief active pulses every 15 minutes
(host awake + sensor reads + Notecard I²C transaction), and one longer radio
burst per outbound sync.
Cellular session profile: Cellular transmit sessions average approximately 250 mA at the 5V rail for 10–60 seconds per outbound sync. Over a 4-hour summary cycle (16 sample wakes), expect the trace to show 16 short pulses of tens of mA (host active plus ADC reads, ~3–5 seconds each). Between them are 15-minute stretches at the system quiescent floor (Notecard ~18 µA + host in STOP2 ~1–2 µA + the MB7389's standby draw), plus one ~250 mA burst when the cellular session opens. Total energy for the 4-hour window comes to roughly 10 mAh from the 5V rail, and most of it is the MB7389's ~2.3 mA ranging current, not the radio.
Satellite session profile: On the Skylo NTN path, the power picture looks quite different. The NOTE-NBGLWX idles at the same ~18 µA @ 5V as in cellular mode, but the modem-active phase of a satellite sync lasts several minutes instead of the 10–60 seconds of a typical cellular session. Per-session satellite energy is higher because the session runs longer, not because the current at any moment is higher. The NOTE-NBGLWX datasheet (Power Information section) puts modem-active draw at approximately 250 mA @ 5V. It doesn't publish a separate NTN-specific sub-phase current, because the BG95-S5 NTN modem-active draw falls within the same envelope as cellular. The VMODEM_P supply has to sustain 750 mA to cover peak modem activity in either mode:
| Phase | Current draw | Typical duration |
|---|---|---|
| Idle (between sessions, cellular and satellite radios in standby) | ~18 µA @ 5V | continuous between syncs |
| Cellular session (LTE-M/NB-IoT acquisition + data transfer) | ~250 mA average @ 5V | 10–60 seconds per outbound sync |
| Satellite acquisition + NTN registration (GEO search + Skylo attach) | ~250 mA average @ 5V. The BG95-S5 NTN modem-active draw is in the same range as cellular, and the NOTE-NBGLWX datasheet doesn't break out sub-phase variation across search, attach, and idle-wait between attempts | 2–5 minutes nominal, up to 10 minutes with marginal sky view or poor satellite geometry |
| Satellite data transfer (NTN payload upload to Skylo) | ~250 mA average @ 5V | tens of seconds, following successful acquisition |
Working from the published 250 mA figure, a 30-second cellular sync draws approximately 2 mAh at the 5V rail. A 3-minute satellite session (acquisition + data transfer combined) draws approximately 12.5 mAh, which is roughly 6× more per sync. At the default 4-hour summary interval, six cellular syncs a day add up to approximately 12 mAh of radio energy, and six satellite syncs come to approximately 75 mAh a day. If satellite is the primary or a frequent transport at your site, size the solar panel and battery for the ~75 mAh/day satellite figure, not the ~12 mAh/day cellular one.
A satellite-primary Mojo trace shows an ~18 µA floor between syncs, then a multi-minute plateau at approximately 250 mA average (satellite acquisition + Skylo NTN registration + data transfer) before the radio goes back to idle. It looks nothing like the short cellular burst. On the trace, the acquisition and data-transfer sub-phases show up together as one long block at modem-active current. If acquisition stalls because of poor sky view or satellite geometry, the plateau simply runs past the 2–5 minute nominal, with no step-change in current you could see on the trace. If the site has reliable cellular, the satellite path never gets used and the cellular profile applies.
If the Mojo trace shows a steady tens-of-mA draw that never drops to idle, the
host MCU isn't entering STOP2. Check the ATTN→D5 jumper, and confirm the
firmware was built with USB support set to None (usb=none).
Validating the satellite path. To confirm satellite delivery before field
deployment, take the device somewhere with no usable cellular coverage and
trigger a tank_alert.qo Note by lowering level_alert_pct to 99 in Notehub.
With a clear view of the equator-facing sky, the NOTE-NBGLWX switches to the
Skylo satellite path. Check that the alert Note shows up in Notehub. Its body
is the same as a cellular-delivered Note, but the session metadata reflects the
satellite transport. Setting up the session can take several minutes, so give
it up to 10 minutes before you decide satellite isn't working. The usual
culprits are an antenna that's blocked or not pointed toward the equator,
obstructions between the antenna and the horizon, or a device sitting too
close to a building or tree line. The Satellite Best Practices
guide has a complete troubleshooting
checklist.
10. Troubleshooting
| Symptom | Root Cause | Fix |
|---|---|---|
No tank_status.qo Notes appear in Notehub after 4 hours | Device hasn't synced yet, so check connectivity | If you're indoors or in a valley, move the antenna outside where it can see the sky. For cellular, confirm you're in LTE-M/NB-IoT coverage (check a carrier map). For satellite, confirm the antenna has an unobstructed view of the equator-facing sky. |
level_pct reads 0% or 100% regardless of actual tank fill | tank_depth_mm or sensor_min_mm not calibrated correctly | Follow the calibration procedure in §9. Hold the tank full, then completely empty, for one 5-minute window each while sampling every 60 seconds. |
level_pct fluctuates wildly or reads negative | Sensor is outside the 300–5000 mm range, or the mounting is unstable | Check that the MB7389 is mounted vertically (face pointing straight down) with at least 300 mm of clearance above the maximum water surface. Secure the mounting bracket so it doesn't vibrate. |
pump_amps reads 0 even when the pump is running | CT isn't clamped around the pump supply line, or the CT jaws are open | Make sure the CT jaws are fully closed around a single conductor of the pump supply cable. Check the bias circuit wiring (10 kΩ divider, 10 µF cap, Notecarrier A1). Put a calibrated clamp meter on the same conductor to confirm current is flowing. |
Device sends an alert every 15 minutes instead of respecting alert_cooldown_sec | Alert cooldown timers were cleared by a reboot (power interruption), or alert_cooldown_sec is set lower than you expect | The firmware skips alert checks until the Notecard has network time, but a reboot clears the cooldown timers, so the first wake after each reboot can send an alert right away. If alerts arrive on every wake, the host may be rebooting over and over, so look for brownouts during radio sessions. If it isn't, check alert_cooldown_sec in Notehub and confirm it shows up in env.get responses (enable TANK_MONITOR_DEBUG in the sketch to see I²C traces). |
| Battery drains in days instead of weeks, and Mojo shows a continuous tens-of-mA draw | Host isn't entering STOP2 | Confirm the ATTN→D5 jumper is fitted (see §5) and that the firmware was built with USB support set to None (usb=none). With the USB CDC stack enabled and no USB host attached, the host leaves STOP2 within a millisecond of entering it. |
| Device stops reporting after several days | Battery voltage has dropped below the critical-discharge threshold | Make sure the solar panel is unobstructed and getting enough sunlight, and that the charge controller's LOAD output is on and passing battery voltage (about 12V) to the step-down regulator. If the panel or controller is faulty, the battery runs down until the controller's low-voltage disconnect switches off the LOAD output, and the device goes silent. Notehub's last-seen timestamp tells you roughly when the device lost power. |
| Satellite acquisition times out (no Note appears in 10+ minutes) | Antenna has poor or no sky view, or the device is too close to structures that block the horizon | Move the antenna outdoors or up high where it has an unobstructed view of the equator-facing sky (south in the northern hemisphere, north in the southern hemisphere). See the Satellite Best Practices guide. If the site really has no equator-facing sky view, satellite won't work there, so confirm you have cellular coverage to fall back on. |
11. Limitations and Next Steps
This reference design is a single-tank monitor on a pole next to a stock tank. It isn't a multi-tank irrigation controller, a herd-level analytics platform, or a satellite design for polar regions. The list below spells out the limits that come with that scope, so a rancher or integrator can see exactly where the design ends and where production work begins.
Simplified for this reference design
These simplifications are on purpose. Once the basic single-tank monitor has proven itself in the field, they're the spots where a rancher or integrator heading to production will want to add a sensor, a calibration step, or an integration.
Satellite antenna siting is site-specific. The NOTE-NBGLWX Skylo path needs a clear, unobstructed view of the equator-facing sky (the southern sky in the northern hemisphere, the northern sky in the southern hemisphere). A tank in a valley, near a tree line, or next to a structure that blocks the horizon toward the equator may get degraded satellite performance or no satellite link at all. Before you pick this design for a deployment that depends on satellite, survey the site for equator-facing sky exposure. Sites that need Iridium coverage (polar regions, or anywhere Skylo GEO coverage doesn't reach) need a different satellite module.
Satellite data budget is worth watching on any deployment that spends a lot of time on the satellite path. Keep an eye on satellite data use in the Notehub usage dashboard, and check the Blues pricing page for current satellite data allotments and usage terms.
Manual calibration is part of commissioning. Someone has to measure
tank_depth_mm and sensor_min_mm at each installation and enter them in
Notehub. A production design would add a commissioning sequence that measures
and stores these values automatically during the first fill cycle.
Single-phase current sensing is all you get from one CT, because it clamps around one conductor. Three-phase deep-well pump motors, which are common in high-volume agricultural wells, need three CTs and a firmware change that sums the three RMS values.
No pump fault-to-start detection. The firmware can tell when the pump is running (current above threshold), but it can't tell whether the pump should be running. A float-switch digital input on a host GPIO would make a "pump failed to start" alert possible, firing when the float says the tank needs water and there's no pump current.
No local sample history. If both cellular and satellite are down, the Notecard queues Notes and can hold them for multi-day transmission. The firmware, though, only keeps the current summary window in RAM. A device reset (a lightning strike or power interruption) clears the in-progress accumulator, and the partial window's data is lost.
Mojo is bench-validation equipment. The firmware doesn't read the Mojo's
LTC2959 coulomb counter over I²C at runtime, and alerts is the only
meta-field in the summary. Adding a mojo_mah field for fleet-level power
telemetry would be a straightforward extension.
Production Next Steps
Once a rancher has the basic tank monitor running, these are the next things to build. They start with the detection improvements that pay off soonest and work toward the deeper fleet-management integrations.
Float-switch input on a digital GPIO adds "pump should be running but isn't" detection and closes the gap that current-only sensing leaves.
Over-the-air firmware updates through Notecard Outboard DFU matter a lot for a device that sits in a pasture for years between visits.
Three-phase CT support brings high-volume wells into scope. Three CTs would
go on A1, A4, and A5 (A3 is taken by the battery-divider PMOS enable), and the
firmware would sum the three RMS values into a single pump_amps field.
A per-device commissioning wizard, a "first-boot setup mode," would walk the installer through measuring and storing calibration values before the device starts normal operation.
env.get with the time argument fetches environment variables only when
they've changed since the last sync, which cuts I²C overhead on each wake.
Satellite data usage monitoring, through a Notehub route or an environment variable feedback loop, would warn operators when satellite data use gets close to the plan's included allotment.
12. Summary
A Notecarrier CX and Notecard for Skylo, along with a weatherproof ultrasonic level sensor, a clamp-on current transformer, and a handful of passive components, let a rancher keep an eye on every stock tank on the property without driving the pasture roads. Notecard for Skylo uses cellular where a tower is in reach and falls back to the Skylo satellite network where terrestrial coverage ends. Tanks in covered valleys and tanks on remote ridges both report through the same firmware and the same Notehub project. The device samples locally every 15 minutes with the host MCU asleep in between, and the radio only wakes for the 4-hour summary or an immediate alert. With that duty cycle the system can run indefinitely on a modest solar setup, and its power-aware sleep extension carries it through long overcast stretches in northern winters. Dry-tank and solar fault alerts reach the rancher's phone before the cattle start to suffer from dehydration. Pump current rides along as observational telemetry in every summary and alert payload, so operators can line it up against tank levels after the fact. Knowing when the tank is low is a simple problem, and the Blues stack keeps the answer just as simple: a sensor, a Notecard, and a network path that follows the asset wherever it lives.