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_rightOff-Grid Livestock Water Tank Monitor

Off-Grid Livestock Water Tank Monitor

Off-Grid Livestock Water Tank Monitor 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/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

System architecture: ultrasonic level sensor + pump CT + battery voltage divider → Notecarrier CX with STM32 host and NOTE-NBGLWX → cellular or Skylo satellite → Notehub → SMS / time-series DB / trends

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

  1. Assemble hardware. Order the parts in the §4 BOM, build the circuit from §5, and mount the enclosure on the tank post.
  2. Flash firmware. Clone this repo, put your Notehub ProductUID in the sketch, jumper the Notecarrier CX's ATTN pin to D5 (see §5), then build and flash:
    arduino-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_monitor
    The usb=none board option turns off the USB CDC stack, which has to be off for the host to sleep (see §7). These board options match firmware/livestock_water_tank_monitor/sketch.yaml, so you can also leave out --fqbn when you run the commands from the sketch directory.
  3. 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.qo Note shows up in Notehub, measure your actual tank geometry and set tank_depth_mm and sensor_min_mm as environment variables in Notehub (§6 step 4 has the path). After the next sync, level_pct reports accurate percentages.
  4. Set routes. In Notehub, add two routes, one for tank_alert.qo (to an SMS or push service) and one for tank_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

PartQtyRationale
Notecarrier CX1Integrated 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)1Cellular (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 Mojo1Bench-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)1IP67-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-output1Clamp-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)1Breaks 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)2Bias 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 capacitor1Decouples 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)1High-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)1Shifts 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%1High 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)2One 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, ¼ W1BSS84 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)1Makes 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 Ah1Power 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 Controller1Handles 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 Panel1Sized 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.1Needs 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 fitting1 eachCarries 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″1Holds 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

Wiring: MB7389 ultrasonic to A0; SCT-013 CT with V/2 bias to A1; battery divider to A2 with PMOS gated by A3; ATTN → D5 jumper so the Notecard can wake the host from STOP2; Skylo MAIN antenna via u.FL to SMA bulkhead; solar 20 W → charge controller → 12 V battery → 5 V step-down → +VBAT

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.

warning

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 → Notecarrier GND
  • MaxBotix MB7389 AN (analog voltage output pin) → Notecarrier A0
  • Leave the MB7389 TX (TTL serial) and PW (pulse-width) pins unconnected, since this design only uses AN.

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 → Notecarrier GND. 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 → Notecarrier A1
  • 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

  1. 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.ino as PRODUCT_UID.

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

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

  4. 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_mm and sensor_min_mm). You have to set them for each installation to get accurate level percentages.

    VariableDefaultPurpose
    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_low alert fires. The rancher still has time to act.
    level_critical_pct10Tank level (%) below which a level_critical alert 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_low alert 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, lower sample_interval_sec to match (see §9). When this value changes, the firmware notices on the next wake and re-issues hub.set with 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_low and level_critical alerts. The battery_low alert is edge-triggered instead and ignores this cooldown. It fires once per episode and only re-arms after the voltage recovers above battery_alert_v + 0.5V.
  5. 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 for tank_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
    }

    alerts is the number of tank_alert.qo Notes sent since the last summary. pump_on comes from comparing the window-average pump_amps against the pump_on_amps threshold.

7. Firmware Design

The firmware is four files in the firmware/livestock_water_tank_monitor/ directory, and the .ino is the entry point:

FileRole
livestock_water_tank_monitor.inoEntry 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.hCompile-time constants, the GlobalState struct, extern declarations for all cross-file globals, and helper function prototypes.
livestock_water_tank_monitor_helpers.cppImplementations of the sensor-read, env-var parsing, alert, and summary helpers.
cx_sleep.hHost sleep: card.attn request, STOP2 entry, ATTN wake.

Dependencies:

  • Arduino core for STM32 (stm32duino/Arduino_Core_STM32).
  • Blues Wireless Notecard, the note-arduino library. Install it from the Arduino Library Manager or download it from the note-arduino releases.
  • STM32duino Low Power and its dependency STM32duino RTC, both from the Library Manager. Low Power provides the STOP2 entry and ATTN-pin wake used in cx_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

ResponsibilityFunction
Notecard configuration (hub.set, card.transport, env.template, card.motion.mode)notecardConfigure
Re-issue hub.set when summary_interval_min env var changesreapplyHubSet
Template registration for both NotefilesdefineTemplates
Environment-variable fetch (each wake)fetchEnvOverrides
Ultrasonic raw distance readingreadDistanceMm
Distance-to-level-percent conversionreadLevelPct
Pump RMS currentreadPumpAmps
Solar battery voltagereadBatteryV
Alert evaluation and emissionevaluateAlerts, sendAlert
Periodic summary emissionsendSummary
Host sleep (STOP2 until the Notecard raises ATTN)cx_sleep.h: cxSleepBegin, cxSleepUntilAttn
Battery-adaptive sleep durationdoSleep
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
  }
}
note

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
}
note

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 outbound value it sent to hub.set in GlobalState (appliedSummaryIntervalMin). After each env.get, if summary_interval_min doesn't match the stored value, reapplyHubSet() re-issues hub.set. That keeps the cellular outbound cadence in step with the summary interval the operator set, with no physical reboot.
  • A single templatesInstalled flag in GlobalState tracks both note.template registrations (summary and alert). The flag only gets set when both templates succeed. If either registration fails on cold boot, the flag stays false and loop() 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.0 as 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.time returns 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_low and level_critical alerts are cooldown-gated (one per alert_cooldown_sec, 1 hour by default) so a slowly draining tank doesn't page the rancher over and over. battery_low is edge-triggered. It fires once on the first crossing below battery_alert_v and stays quiet until the voltage recovers above battery_alert_v + 0.5V. That keeps the 60-minute emergency-sleep cadence from opening a sync:true radio 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., alerts field, range −32,768 to +32,767)
  • true = boolean field (e.g., pump_on)
  • 14 = Unix timestamp field, which the Notecard fills in automatically on each note.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

Data flow: 15-min sample of level / pump amps / battery V → three rules (level_low, level_critical, battery_low) → tank_alert.qo (sync:true) and tank_status.qo (every 4 h templated) → Notehub routes

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) every summary_interval_min (4 hours by default, 6 Notes per day). Each numeric field (level_pct, distance_mm, pump_amps, and battery_v) is the mean of the valid samples collected over the window. A sensor with zero valid reads in the window reports -1.0 as a sentinel, and alerts carries the number of alert Notes sent since the last summary. pump_on is a derived boolean set at summary time. It's true when the window-average pump_amps is at or above pump_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 with sync:true whenever a threshold trips. The Skylo NTN satellite path needs the compact template and explicit port, and the same note.add goes out over cellular or satellite depending on which transport the Notecard picks. alert_code is an integer field (0 = level_low, 1 = level_critical, 2 = battery_low). level_low and level_critical are cooldown-gated (one per alert_cooldown_sec, 1 hour by default) so a slowly drifting tank doesn't generate hundreds of pages. battery_low is edge-triggered. One alert fires on the first crossing below battery_alert_v, and no more fire until the voltage recovers above battery_alert_v + 0.5V.

Alert triggers.

  • level_low: tank fill percentage below level_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 below level_critical_pct (10% by default). This one is urgent, because cattle dehydration risk is hours away, not days.
  • battery_low: solar battery voltage below battery_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:

PhaseCurrent drawTypical duration
Idle (between sessions, cellular and satellite radios in standby)~18 µA @ 5Vcontinuous between syncs
Cellular session (LTE-M/NB-IoT acquisition + data transfer)~250 mA average @ 5V10–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 attempts2–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 @ 5Vtens 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

SymptomRoot CauseFix
No tank_status.qo Notes appear in Notehub after 4 hoursDevice hasn't synced yet, so check connectivityIf 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 filltank_depth_mm or sensor_min_mm not calibrated correctlyFollow 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 negativeSensor is outside the 300–5000 mm range, or the mounting is unstableCheck 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 runningCT isn't clamped around the pump supply line, or the CT jaws are openMake 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_secAlert cooldown timers were cleared by a reboot (power interruption), or alert_cooldown_sec is set lower than you expectThe 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 drawHost isn't entering STOP2Confirm 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 daysBattery voltage has dropped below the critical-discharge thresholdMake 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 horizonMove 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.

On This Page

  • What You'll Build
  • 1. Project Overview
  • 2. System Architecture
  • 3. Technical Summary
  • 4. Hardware Requirements
  • 5. Wiring and Assembly
  • 6. Notehub Setup
  • 7. Firmware Design
    • Modules
    • Sensor reading strategy
    • Event payload design
    • Low-power strategy
    • Retry and error handling
    • Key code snippet 1 — template registration
    • Key code snippet 2 — immediate alert with sync
    • Key code snippet 3 — battery-adaptive sleep until ATTN
    • Key code snippet 4 — dynamic hub.set cadence
  • 8. Data Flow
  • 9. Validation and Testing
  • 10. Troubleshooting
  • 11. Limitations and Next Steps
    • Simplified for this reference design
    • 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