Off-Grid Solar Battery Site Controller

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/solar_battery_controller/ so it can learn how the device works and help you adapt it to your hardware.
This project is a Blues battery management systems reference design for remote off-grid sites powered by solar arrays and battery banks, and it monitors them at the bank level. A Blues Notecarrier CX reads two Victron VE.Direct devices: a SmartShunt for battery-bank metrics and a SmartSolar MPPT charge controller for solar-side metrics. Every four hours it reports bank-level state of charge (SoC), daily solar harvest, load draw, battery temperature, and charge state to the Blues Notehub cloud service, and it sends alerts right away, before the site goes dark. A Blues Notecard seated in the carrier's M.2 slot handles connectivity. Two SKUs run the same firmware source, with one build flag for the satellite SKU: Notecard Cell+WiFi (NOTE-MBGLW) for sites within terrestrial cellular range, and Notecard for Skylo (NOTE-NBGLWX) for satellite fallback at sites where cellular coverage is marginal or absent.
1. Project Overview
The problem. Cell towers at the edge of coverage, environmental monitoring
stations in the backcountry, and off-grid cabins all run on solar and battery,
and they tend to fail the same way. A problem starts small, days before anyone
notices, and builds until the site goes dark. Maybe the PV array is slowly
degrading, or a load crept up after a firmware update to on-site equipment, or
the battery bank's capacity has quietly faded with age. None of these is
catastrophic on its own, but any one of them can drain a bank the solar array is
no longer sized to refill. Because the site is remote by definition, a recharge
deficit that lasts for days stays invisible without continuous telemetry. This
design detects that condition directly. If the charge controller doesn't reach a
full-charge state (Float, Absorption, Equalize, or Auto Equalize) for a
configurable number of consecutive days, a harvest_deficit alert fires before
the bank is depleted, so the operations team has time to respond before the site
goes dark.
Interface Note. This project reads the Victron VE.Direct text protocol, a one-wire broadcast interface that exposes battery-bank aggregates (SoC, voltage, current, temperature, and charge state) and solar-side metrics (panel voltage, power, and daily yield). Those are the signals you need to catch the site-uptime failure modes this design targets: persistent recharge deficit, low SoC, thermal overtemperature, and excessive load draw. For a bank-level site-uptime monitor, bank-level aggregates are the right scope. Cell-imbalance monitoring was explicitly evaluated during design and scoped out. Per-cell voltages and imbalance data aren't broadcast on the VE.Direct wire. They travel over CAN bus and need a dedicated CAN controller and transceiver, and this hardware stack has neither. Cell-level telemetry is a separate hardware and firmware problem, so it belongs in a companion design, not in an extension of this project (see §11 for the full rationale and the companion-design specification).
Why Notecard. These sites are off-grid and off-network by definition. There's no building WiFi to join, no Ethernet jack in the enclosure, and no cellular router whose monthly bill someone else is paying. Notecard manages its own radio session, draws microamp-range idle current between transmissions, and doesn't need anyone from site IT to set it up. Two SKUs fit the same M.2 slot on the Notecarrier CX and run the same firmware source:
New to Blues?
Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.
Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.
- Notecard Cell+WiFi (NOTE-MBGLW): LTE Cat-1 bis cellular with opportunistic WiFi fallback. It's the practical pick for bench validation and for sites with decent terrestrial coverage.
- Notecard for Skylo
(NOTE-NBGLWX): LTE-M cellular
plus Skylo Non-Terrestrial Network (NTN) satellite
connectivity on a single card. Build with
-DSKYLO_BUILDand the firmware turns on the satellite fallback withcard.transport, and from there the Notecard picks the radio on its own. Both antennas must be positioned outdoors with an unobstructed view of the sky. §4 and §5 cover the mounting and enclosure-feedthrough requirements. Notecard for Skylo is the main production SKU for the truly remote towers, wilderness arrays, and high-altitude installations this use case targets.
Deployment scenario. A Notecarrier CX mounts inside the existing site enclosure or in a weatherproof add-on box. It's powered from the site's 5V regulation bus or from a small DC-DC converter off the main battery bus. Two short VE.Direct cables run from the Notecarrier CX dual-row header to the SmartShunt (the battery shunt, usually mounted near the battery bank's negative terminal) and to the SmartSolar MPPT controller (often mounted on the enclosure wall). Nothing on the Victron equipment changes, and the solar system keeps running the whole time.
2. System Architecture
Device-side responsibilities. The STM32L433 host on the Notecarrier CX
spends almost all of its time asleep in STM32 STOP2, drawing a few microamps
with its RAM intact. Every 15 minutes (configurable), the Notecard's ATTN pin
wakes it up. The host reads one VE.Direct frame from each Victron device over
UART, adds them to rolling averages in a state struct that just stays in RAM
across sleep cycles, checks four alert rules locally, and goes back to sleep. A
full cycle takes under 8 seconds. The Notecard talks to the host over I²C, which
keeps serial buffers, AT commands, and a modem state machine out of the host
firmware.
Notecard responsibilities. The Notecard handles all the connectivity work,
and the host never sees any of it. It holds outbound
Notes in its on-device queue and wakes the
radio on the configured
hub.set outbound
cadence (4 hours by default). When a sync:true alert lands in the queue, the
Notecard breaks that cadence and sends it right away. On each inbound sync it
also pulls environment
variables
down from Notehub, so you can retune alert thresholds and sample intervals
without a firmware update or a site visit.
Notehub responsibilities. The Notecard's embedded global SIM connects
through whatever carrier reaches the site, and Notehub
takes in every event that arrives. Alerts (solar_alert.qo) and summaries
(solar_summary.qo) land in separate
Notefiles, so routes can send them out at
different urgencies with no filter logic. Alerts go straight to the NOC or
on-call system, and summaries go to a long-term analytics store for trend work.
Routing to the cloud (high level). Notehub can route to HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other targets. Route setup is specific to your project, and this project doesn't ship a downstream endpoint, so start with the Notehub routing docs.
3. Technical Summary
You can have a working monitor running in ~2 hours:
- Assemble the Notecarrier CX with the Notecard (Cell+WiFi or Skylo), the DC-DC converter, and the resistor dividers listed in §4.
- Wire the SmartShunt and MPPT to the two VE.Direct ports on the Notecarrier CX header (§5).
- Install dependencies:
arduino-cli core install "STMicroelectronics:stm32" arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC" - Set ProductUID on the
#define PRODUCT_UIDline infirmware/solar_battery_controller/solar_battery_controller.ino, then fit theATTN→D5jumper on the Notecarrier CX header (§5). - Flash:
The
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/solar_battery_controller/ arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/cu.usbmodem* firmware/solar_battery_controller/usb=noneboard option turns off the USB CDC stack, which has to be off for the host to sleep (see §7.1). - Claim and configure in Notehub:
- Power on. The device claims itself to your project within ~1 minute (watch the Devices tab).
- Create a Fleet and set its Fleet → Environment variables (see the table in §6). The defaults are fine for initial testing.
- Add routes for
solar_alert.qo(real-time) andsolar_summary.qo(storage/analytics).
- Validate:
_session.qoandsolar_summary.qoNotes should show up in Notehub within 5–10 minutes (see "What you should see in Notehub" in §6).
Here's a sample Note this device emits:
{
"bat_v": 26.1,
"bat_a": -3.2,
"bat_w": -83.5,
"soc_pct": 78.4,
"bat_temp_c": 22.1,
"pv_v": 34.8,
"pv_w": 142.0,
"load_w": 225.5,
"yield_kwh": 1.24,
"ttg_min": -1,
"cs": 5
}4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an onboard STM32L433 host, so there's no separate MCU. Provides the UART (Serial1), analog, and SPI/I²C headers this sensor mix needs. The host sleeps in STOP2 between wakes, and the Notecard's ATTN pin wakes it. |
| Notecard Cell+WiFi (NOTE-MBGLW) | 1 (bench / sites with reliable cellular coverage) | LTE Cat-1 bis cellular with opportunistic WiFi fallback, plus a prepaid global SIM with no monthly commitment. Use this SKU for bench validation and for field sites with reliable terrestrial cellular coverage. |
| Notecard for Skylo (NOTE-NBGLWX) | 1 (sites with marginal or no terrestrial coverage) | LTE-M cellular plus Skylo NTN satellite on a single M.2 card. It's the production SKU for remote towers, backcountry arrays, and high-altitude installations where terrestrial cellular is unreliable. It needs both the Notecard-certified satellite/LTE antenna and a passive GPS/GNSS antenna, and both must be mounted outdoors with an unobstructed view of the sky (§5 covers enclosure feedthrough). It runs the same firmware as the NOTE-MBGLW, built with -DSKYLO_BUILD (§7.1). Read the Notecard for Skylo datasheet and the antenna guide before deploying. |
| Blues Mojo | 1 | Coulomb counter on the +VBAT rail for checking the sleep/wake current profile during bench bring-up and commissioning (see §9). |
| Victron SmartShunt 500A/50mV | 1 | Industry-standard battery shunt monitor. Reports battery voltage, current, SoC, and optional temperature over VE.Direct UART. The 500A variant covers the vast majority of off-grid installations, from small cabins to telecom sites, and there's a 1000A variant for high-current systems. Includes a VE.Direct cable. |
| Victron SmartSolar MPPT 75/15 | 1 (or existing) | Solar charge controller. Reports panel voltage, instantaneous panel power, daily yield, and charge state over VE.Direct. Pick the MPPT SKU that matches your array size, since the VE.Direct protocol and field names are the same across the SmartSolar family. Includes a VE.Direct cable. |
| Victron VE.Direct cable | 0–2 | 4-pin JST PH 2.0 to bare-wire cable. Each Victron device above comes with one, so only buy extras if your layout needs longer runs. Wire colors aren't standardized, so always verify pin assignments with a multimeter before connecting (see §5). |
| 10 kΩ, 20 kΩ resistors | 2 each | One voltage-divider pair per VE.Direct RX line: device TX → 10 kΩ → MCU RX pin, and junction → 20 kΩ → GND. Output ≈ 3.33 V. VE.Direct is a one-way broadcast protocol where the host only reads and never writes, so a simple resistor divider is the right, lowest-risk interface for this one-way UART. §5 has the pin-by-pin wiring. |
| Victron Temperature Sensor for SmartShunt | 0–1 | Plugs into the SmartShunt's temp sensor port. You need it to populate bat_temp_c in the summary Note. It's optional, but strongly recommended for lithium battery banks, where thermal runaway is a real risk. |
| Traco TSR 1-2450 DC-DC converter (12 V and 24 V sites) | 1 | 6.5–36 V input, 5 V / 1 A output, SIP-3 package. The input range covers 12 V and 24 V battery buses through their full charge/discharge swing. The TSR 1 datasheet covers derating above 40 °C. |
| Traco TSR 1-4850 DC-DC converter (48 V telecom sites) | 1 | 18–75 V input, 5 V / 1 A output, same SIP-3 footprint as the TSR 1-2450. Handles 48 V telecom battery buses at full charge. Use this part instead of the TSR 1-2450 on 48 V deployments. Nothing else changes. |
| Hammond 1555JGY weatherproof enclosure | 1 | 125 × 95 × 57 mm, ABS, IP67, gray. Fits the Notecarrier CX, the resistor divider parts, and the DC-DC converter. Mount it inside the site's existing primary enclosure or next to the SmartShunt. |
| Skylo-certified cellular/NTN antenna, included with the NOTE-NBGLWX kit (MAIN u.FL port) | 1 (NOTE-NBGLWX only) | Ships in the NOTE-NBGLWX kit and connects to the MAIN u.FL port. Do not substitute it. The Notecard is certified on Skylo's network only with this antenna, and replacing it leaves the device uncertified, so Skylo may block it from the network. If you need a different antenna, it has to go through a Skylo delta-certification test. Contact Blues for recommended test houses. Mount it outdoors with an unobstructed view of the sky (see §5). |
| Blues Flexible Dual LTE/WiFi and GPS/GNSS Antenna (Quectel YCA001BA) (NOTE-NBGLWX GPS port) | 1 (NOTE-NBGLWX only) | Passive flexible multi-band antenna with a u.FL connector. Its GNSS L1 coverage (1560–1620 MHz, including GPS L1 at 1575 MHz) suits the GPS u.FL port on the NOTE-NBGLWX. Unless you set a fixed location (see Fixed GPS location below), the NOTE-NBGLWX needs a GPS fix from this antenna before each satellite session. Use only a passive (un-amplified) antenna on this port, because active/LNA GNSS antennas don't work with the NOTE-NBGLWX GPS input. Route the flexible cable lead through a cable gland in the enclosure wall and mount the antenna flat on the enclosure exterior with a clear view of the sky. |
| u.FL to SMA female pigtail, ≥100 mm, RG316 or RG178 (e.g. SparkFun WRL-09145) + IP67-rated SMA bulkhead connector (NOTE-NBGLWX MAIN feedthrough) | 1 (NOTE-NBGLWX only) | Routes the included Skylo antenna from the Notecard's MAIN u.FL port through the enclosure wall by way of an SMA bulkhead connector. Use the shortest pigtail that reaches the nearest enclosure wall without sharp bends, which keeps insertion loss on the satellite band low. DigiKey and Mouser carry IP67-rated SMA panel-mount bulkhead connectors. The GPS antenna (Quectel YCA001BA above) has a flexible lead long enough to go straight through its own cable gland, so the GPS port doesn't need another pigtail. |
| M16 IP67 cable gland, suitable for 4–8 mm cable OD (NOTE-NBGLWX GPS feedthrough) | 1 (NOTE-NBGLWX only) | Seals the GPS antenna (Quectel YCA001BA) cable lead where it passes through the enclosure wall. The MAIN antenna goes through the SMA bulkhead connector listed above, which has its own weather seal. Thread and bore size have to match your enclosure wall thickness. DigiKey, Mouser, and electrical distributors carry nylon M16 glands. |
| Cellular/LTE antenna with u.FL connector (see Blues antenna guide for certified options) | 1 (NOTE-MBGLW) | You have to connect a u.FL cellular antenna to the NOTE-MBGLW's MAIN u.FL pigtail lead on the Notecarrier CX, because the carrier has no onboard PCB trace cellular antenna. A compact flexible u.FL LTE antenna works for non-metallic (plastic or ABS) enclosures. For metallic enclosures, add a u.FL-to-SMA pigtail and an IP67-rated SMA bulkhead connector to get the antenna outside the enclosure wall. The Blues antenna guide covers which antennas are compatible and where to put them. |
All Blues hardware ships with an active SIM that includes 500 MB of data and 10 years of service, with no activation fees and no monthly commitment.
5. Wiring and Assembly
All host I/O uses the Notecarrier
CX dual-row 16-pin
header. A short jumper between ATTN and D5 on that header lets the Notecard
wake the sleeping host. The Notecard (Cell+WiFi or Skylo; see §4) seats in the
carrier's M.2 slot, and the Mojo sits inline on the +VBAT power rail for bench
bring-up and commissioning. A simple 10 kΩ/20 kΩ resistor divider on each RX
line handles VE.Direct TX-to-MCU level shifting. It's the only documented and
validated interface for this project. Active level-shifter boards with
bidirectional MOSFETs aren't recommended for a one-way UART like this.
Safety with high-current DC systems. Off-grid battery banks can source thousands of amps into a short circuit. Before making any connections in or near battery, shunt, or MPPT wiring, disconnect the solar array at the MPPT PV input terminals. Verify polarity at every connection point with a multimeter before making contact, and have a qualified installer review all DC wiring before commissioning. The VE.Direct signal connections below are low-voltage and carry microamp-level current. They don't interrupt the high-current path, but they pass through the same enclosure as live battery wiring.
VE.Direct cable pinout
Each Victron device has a 4-pin JST PH 2.0 (2.0 mm pitch) port for VE.Direct. Here's the standard pin numbering, with pin 1 at the locking-tab end of the connector:
| Pin | Signal | Direction | Notes |
|---|---|---|---|
| 1 | GND | N/A | Connect to Notecarrier CX GND |
| 2 | RX (into device) | Host → Device | Leave NC, since the protocol is device-broadcast only |
| 3 | TX (out from device) | Device → Host | 5V logic, so it needs level shifting or a divider |
| 4 | +5V (from device) | Device → Host | Leave NC, since the host is externally powered |
Wire color warning. Wire colors on Victron VE.Direct cables aren't standardized across product revisions, and they can mislead you (red may be GND, not power). Before connecting anything to the STM32 or level shifter, always confirm the pin assignment with a multimeter. Measure the DC voltage between the two outer wires to find pin 4 (+5V). The VE.Direct Protocol Specification is the authoritative reference for your specific device revision.
The standard Victron VE.Direct cable (ASS030530200 / ASS030530300) ends in bare wires on the host side. Connect pin 3 (TX) and pin 1 (GND), and leave pins 2 and 4 unconnected or tape them off.
Pin-by-pin wiring
SmartShunt (Battery shunt / Serial1):
- SmartShunt VE.Direct pin 3 (TX) → 10 kΩ resistor → CX header RX pin, with the junction also connected to → 20 kΩ resistor → GND. The midpoint between the two resistors is the MCU RX input, and it drops 5 V to ~3.33 V.
- SmartShunt VE.Direct pin 1 (GND) → CX header GND.
- Pins 2 and 4 stay unconnected. The protocol is broadcast-only, so the host never writes to the device, and the host is externally powered.
Victron temperature sensor (SmartShunt temp-sensor port): Plug the optional
Victron Temperature Sensor for SmartShunt straight into the SmartShunt's
dedicated temperature-sensor port (a small JST connector on the SmartShunt body,
separate from the VE.Direct port). It doesn't need any wiring to the Notecarrier
CX. The SmartShunt reads the sensor internally and broadcasts the temperature on
the VE.Direct wire, where the firmware picks it up automatically. Without this
sensor, bat_temp_c never shows a real value in summary Notes, and the
temp_high alert can't evaluate.
SmartSolar MPPT (Solar charger / SoftwareSerial on D9):
- MPPT VE.Direct pin 3 (TX) → 10 kΩ resistor → CX header D9, with the junction also connected to → 20 kΩ resistor → GND.
- MPPT VE.Direct pin 1 (GND) → CX header GND.
- CX header D10 is defined as the SoftwareSerial TX pin in firmware but isn't connected to anything. Leave it NC.
ATTN-to-D5 jumper (host sleep):
Fit a short jumper between the ATTN and D5 pins, which sit on the same
16-pin Notecarrier CX header. 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. Don't connect the Notecarrier CX EN pin. 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 just
putting the host to sleep. Putting a Host to Sleep Between Sensor
Readings
walks through the full pattern.
Power, from the battery bus to the Notecarrier CX:
The Traco TSR 1-2450 (12 V and 24 V sites) or TSR 1-4850 (48 V telecom sites) steps the battery bus voltage down to a regulated 5 V rail. The TSR 1 is a non-isolated SIP-3 switcher: Pin 1 = VIN+, Pin 2 = GND (shared input/output return), Pin 3 = VOUT+. Check the pinout against the Traco TSR 1 datasheet before soldering, and don't rely on the silkscreen alone.
Wire the power chain in this order:
- Battery bus positive tap → inline fuse → Traco VIN+ (Pin 1). Install a 2 A glass-cartridge or automotive blade fuse at the battery bus tap point, as close to the battery or busbar as you practically can. The TSR 1 is rated for 1 A output, and a 2 A fuse protects the converter and the 22–24 AWG wiring used at this current level while leaving headroom for inrush. A fuse holder with a visual indicator (for example, the Littelfuse OMNI-BLOK series or equivalent) makes field commissioning easier.
- Battery bus return (negative) → Traco GND/VIN− (Pin 2). Connect the battery-bus return directly to the converter's GND pin, and don't leave the input return floating.
- Traco VOUT+ (Pin 3) → Mojo BAT pin (when Mojo is installed for bench validation; see §9). Route the 5 V output through the Mojo so it measures all the current drawn from the regulated rail. Without Mojo, connect VOUT+ straight to step 4.
- Mojo LOAD pin → Notecarrier CX +VBAT pin. The Mojo passes current through and measures cumulative mAh.
- Traco GND (Pin 2) / system return → Notecarrier CX GND pin. The TSR 1's shared GND is both the input return and the output return, so connect it to the Notecarrier CX GND. It's also the signal ground for the VE.Direct cable shields. Tie the battery-bus return and signal ground together at one point only (at the converter or at the shunt negative terminal, not both) to avoid a ground loop through the VE.Direct cables.
Polarity verification before first power-on. With VIN connected and the VOUT wire to the Notecarrier CX still disconnected, apply power and measure VOUT+ (Pin 3) to GND (Pin 2). You should read 5.0 V ± 2%. If the voltage is missing or reversed, recheck the VIN polarity at Pin 1 and Pin 2 before you connect the Notecarrier CX. Off-grid battery banks can deliver thousands of amps into a reverse-polarity fault, so verify first.
Bench power (USB): During bench bring-up without the DC-DC converter, power the Notecarrier CX from USB through the +VUSB pin or USB-C port. Don't connect +VBAT and +VUSB at the same time.
Cellular antenna (NOTE-MBGLW path):
The Notecarrier CX routes the NOTE-MBGLW's MAIN u.FL port to a u.FL pigtail
lead on the carrier board. Since the Notecarrier CX has no onboard PCB trace
cellular antenna, you have to connect an external u.FL cellular/LTE antenna to
this lead. The Blues antenna
guide lists certified antenna
options.
- Non-metallic (plastic or ABS) enclosure: Connect a compact flexible u.FL LTE antenna and position it inside the enclosure away from metal parts, or route the cable lead through a small cable gland and mount the antenna on the enclosure exterior.
- Metallic enclosure (steel electrical cabinet, aluminum panel): Connect a
u.FL-to-SMA pigtail to the
MAINu.FL pigtail lead, thread an IP67-rated SMA bulkhead connector through the enclosure wall, and mount the cellular antenna on the external SMA port. That gets the RF path outside the metal shell, which would otherwise block a lot of the signal.
The NOTE-MBGLW has a GNSS receiver, but this project doesn't use it. The design runs in cellular mode only, so you don't need a GPS/GNSS antenna. If a future version needs site geolocation, the Notecard's onboard GNSS can be enabled without firmware changes.
Satellite antenna feedthrough (NOTE-NBGLWX only):
- Notecard for Skylo has two u.FL connectors:
MAINfor the Skylo-certified satellite/LTE antenna (included in the kit) andGPSfor the passive GNSS antenna (Quectel YCA001BA). Both antennas must be mounted outside the enclosure with an unobstructed view of the sky, because satellite signals can't get through a metal enclosure wall. - MAIN antenna: Connect the included Skylo antenna to the
MAINu.FL port through the SparkFun WRL-09145 (or equivalent RG316/RG178) pigtail. The pigtail's SMA female end connects to an IP67-rated SMA bulkhead connector threaded through the enclosure wall, and the antenna mounts on the bulkhead's external SMA port. The bulkhead has its own weather seal. - GPS antenna (Quectel YCA001BA): Plug the antenna's u.FL connector into the
GPSu.FL port on the Notecard. Route the flexible cable lead through an M16 IP67 cable gland in the enclosure wall, and mount the flexible patch flat on the outside of the enclosure. - Bend cables gently, since sharp bends kink the coax feed lines. Keep runs as short as practical to hold down insertion loss, especially on the satellite band.
- On the MAIN port, use only the Skylo-certified antenna that comes in the Notecard for Skylo kit. A different antenna voids Skylo network certification and may get the device blocked from the network. The Blues antenna guide has the full certification and placement requirements.
VE.Direct cables are short (0.9 m or 1.8 m), so mount the Notecarrier CX close to the SmartShunt and MPPT to avoid signal integrity problems on long runs.
6. Notehub Setup
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID, which looks like
com.your-company.your-name:solar-battery-site. -
Set the ProductUID in firmware. Open
firmware/solar_battery_controller/solar_battery_controller.inoand replace the empty string on the#define PRODUCT_UID ""line with your project's value. -
Claim the Notecard. Power up the assembled unit. The Notecard associates itself with your Notehub project on its first cellular connection, and the device shows up in the Devices tab within a minute or two.
-
Create a Fleet per site type. Fleets (and Smart Fleets) group devices for shared configuration. One natural split is a fleet for 12V cabin installations and another for 24V telecom sites. Each fleet can carry its own alert thresholds to suit its battery chemistry and load profile.
-
Set environment variables. In Notehub, go to Fleet → Environment (or Device → Environment for a per-unit threshold override). The device pulls the values on its next inbound sync, so you don't need a firmware reflash or a site visit.
Variable Default Purpose soc_alert_pct20.0State-of-charge (%) threshold below which the soc_lowalert fires. Usually 15–25%, depending on battery chemistry.bat_temp_max_c45.0Battery temperature (°C) threshold above which temp_highfires. Only evaluated if a Victron Temperature Sensor is plugged into the SmartShunt temp port. Common values are 50–55 °C for lead-acid and 40–45 °C for lithium.load_alert_w1000.0Load-draw (W) threshold above which load_highfires. Size it to your site's rated load budget. For example, a tower with a 600 W peak load should use 650–700 W for headroom, and a data-center-grade 1.5 kW load should use 1600–1700 W.harvest_deficit_days0.0Consecutive days without the MPPT reaching a full-charge state (Float, Absorption, Equalize, or Auto Equalize) before harvest_deficitfires.0.0disables the check. Try2.0for well-sized systems and3.0–4.0for systems with seasonal or multi-day cloud patterns. Only evaluated when the window has MPPT data.sample_interval_sec900Seconds between host wakes (minimum 60, maximum 3600). Shorter intervals use more power over time but give better resolution. The default of 900 seconds is 15 minutes, and the usual range is 300–1800 s. report_interval_min240(1440withSKYLO_BUILD)Minutes between summary Notes (minimum 15, maximum 1440). When this changes, the firmware resets the window right away and re-issues hub.setso the Notecard's outbound cadence matches on the same cycle. For Skylo NTN deployments, set it to1440(once daily) to cut satellite data use.sync_outbound_min(auto: equals report_interval_min)Notecard outbound sync interval in minutes (15–1440). Leave it unset to follow report_interval_minautomatically, or set it explicitly to decouple the two (for example, sync every 2 hours but summarize every 4). For Skylo, setreport_interval_min=1440and leave this unset.sync_inbound_min(auto: 2 × report_interval_min)Notecard inbound sync interval in minutes (30–2880) for pulling Fleet variable updates. Defaults to 2× report_interval_min. Lower it to deliver threshold changes faster, at the cost of extra satellite or cellular data. -
Configure routes. Add one route for
solar_alert.qo(to an on-call or NOC platform, since these are time-sensitive) and a second forsolar_summary.qo(to a long-term analytics or SCADA store, where four-hour batches are fine). With the two Notefiles split at the source, each one can go to a different downstream system at a different urgency, and you don't need any server-side filtering.
Skylo (NOTE-NBGLWX) deployment guidance
Build the firmware with -DSKYLO_BUILD for the NOTE-NBGLWX (see §7.1).
Notecard for Skylo ships set to wifi-cell and won't touch satellite until it's
told to, so with that flag notecardFirstBoot() sends
card.transport
with method cell-ntn at power-up. From then on the Notecard uses cellular
when it can reach a network and falls back to Skylo on its own. The satellite
transport does differ from cellular in four big ways, though, and those
differences should shape how you configure and commission the device.
Data budget. The NOTE-NBGLWX includes 10 KB of Skylo satellite data, a
one-time allocation rather than a monthly one. Only Notefiles with a compact
template (format: "compact" plus a port) can sync over satellite, so the
firmware registers both of its Notefiles that way: solar_summary.qo on port
50 and solar_alert.qo on port 51. Neither template sets delete, so the same
Notes go out over cellular or satellite, whichever link is up. The compact
summary encodes to roughly 80–120 bytes on the satellite wire, well within the
256-byte Skylo payload ceiling. The firmware doesn't define an env.template,
which Notecard needs before it can receive environment variables over
satellite, so threshold and cadence changes reach the device on its next
cellular session. Inbound syncs cost data too. Each satellite inbound ping costs about 50 bytes,
even when no environment variable changes are pending. At the default 4-hour
summary cadence (6 outbound sessions per day, and 3 inbound sessions per day at
inbound = outbound × 2 = 8 h), a site running only on satellite would use up
the included 10 KB in roughly 11 days. For satellite-primary deployments, keep
report_interval_min at 1440 (once per day), the SKYLO_BUILD default, and
don't override it in the Fleet Environment. Daily summaries use about 4–5 KB per month at normal alert
rates, so the included 10 KB lasts about two months, and satellite data after
that is billed per byte. At-threshold alerts (soc_low, temp_high,
load_high, harvest_deficit) use sync:true and fire immediately no matter
what the summary cadence is. They're the most
valuable transmissions, and there aren't many of them. To cut inbound cost
further, set the _sync_inbound_mins_ntn reserved environment
variable
so the Notecard checks in less often while it's on satellite. The satellite best practices
guide has full data-budget guidance.
Alert latency. On LTE-M, sync:true alerts reach Notehub in
seconds. On the satellite path the Notecard has to get a GPS fix (fast when the
location is fixed; see below), wait for a satellite visibility window, and
complete the NTN session. Expect end-to-end alert latency of 2–10 minutes on
satellite. That's fine for the "site will go dark in the next few hours"
scenario this project targets. Size your NOC response SLAs around it instead of
expecting cellular-speed latency.
Satellite session power. A satellite session runs longer and uses more power than a cellular session. The Notecard has to power the GPS receiver, search for a satellite visibility window, and drive the NTN modem through a transmission. That can mean 2–5 minutes of elevated radio current, compared to the usual 15–30 seconds for LTE Cat-1 bis. With a daily outbound cadence, though, radio sessions drop from 6 per day (the LTE Cat-1 bis default) to 1, which mostly offsets the higher cost per session. Before field deployment, validate the energy profile on the bench with Mojo (§9) at the satellite cadence. The NOTE-NBGLWX power table and expected Mojo trace in §9 show what to look for, and the NOTE-NBGLWX datasheet has SKU-specific power curves.
Fixed GPS location. The NOTE-NBGLWX's satellite module has to know its location before it can register, and by default it gets its own GPS/GNSS fix during each registration. You don't have to configure a location, but on a stationary installation you can skip that fix by locking the position once at commissioning, from the blues.dev In-Browser Terminal or a bench serial session:
{"req": "card.location.mode", "mode": "fixed", "lat": 37.123, "lon": -121.456}Replace lat/lon with the site's actual GPS-derived coordinates. Do not use
tower_lat/tower_lon from _session.qo for this. Those fields are
cell-tower–derived estimates, not GPS fixes, and they can be off by kilometers.
To get accurate coordinates, make sure the GPS antenna is connected and has a
clear view of the sky, then send {"req": "card.location"} from the same
terminal. The lat and lon fields in the response carry the current GPS fix.
Put those values in the card.location.mode fixed command above. Then tell the
Notecard to use that fixed location for satellite sessions with
ntn.gps, since by
default the satellite module gets its own GPS fix:
{"req": "ntn.gps", "on": true}A fixed location gets rid of repeated GPS cold-starts before each satellite session, which cuts both session startup time and power consumption.
Commissioning order. If there's any terrestrial cellular or WiFi coverage at or near the deployment site, power the unit up there first. The Notecard registers with Notehub over LTE-M, confirms all Note templates, and gets an accurate time reference, and each of those speeds up the first satellite session at the final remote location. If the site is beyond all cellular coverage, give the Notecard 5–15 minutes on first power-up to get a GPS fix and complete its initial satellite registration.
First-boot environment variables. At power-up the firmware calls hub.set
to associate the Notecard with your Notehub project, then calls env.get on the
first wake to pull any pre-configured Fleet environment variables. The catch is
that env.get can't return Fleet-level values until the Notecard has completed
an inbound sync with Notehub, and a cold-start device that has never synced
doesn't have any cached env vars yet. On LTE Cat-1 bis the first inbound sync
usually finishes within one to two minutes of power-on, so pre-configured Fleet
variables apply from the second wake onward. At most, you're exposed for one
15-minute sample interval before the right env vars take effect.
That window matters more on NOTE-NBGLWX Skylo deployments, where a single wake
at the 4-hour cellular default can eat a real share of the 10 KB satellite data
budget. The -DSKYLO_BUILD flag covers this too (see §7.1 and the
#ifdef SKYLO_BUILD block in
firmware/solar_battery_controller/solar_battery_controller_notecard_helpers.h).
It changes the compile-time default for DEFAULT_REPORT_INTERVAL_MIN to 1440
minutes, so even the very first wake, before any Notehub inbound sync, uses the
satellite-safe daily cadence. Once the first inbound sync arrives, Fleet
environment variables tune thresholds and cadence from there.
What you should see in Notehub
The Events tab in your project should start filling in within a minute of first power-on:
_session.qo: automatic Notecard housekeeping on each cellular session. It confirms the radio is reaching Notehub. If it never shows up, check the antenna connection andPRODUCT_UID.solar_summary.qo: one perreport_interval_min(every 4 hours by default), with every template field filled in (Notehub leaves out fields that are exactly0; see §7.4). Here's a typical healthy-site body during the day, with solar contributing but load exceeding PV output. The MPPT is in Float state (cs: 5, battery at working SoC) while the battery supplements solar to cover the full site load:A negative{ "bat_v": 26.1, "bat_a": -3.2, "bat_w": -83.5, "soc_pct": 78.4, "bat_temp_c": 22.1, "pv_v": 34.8, "pv_w": 142.0, "load_w": 225.5, "yield_kwh": 1.24, "ttg_min": -1, "cs": 5 }bat_ameans the battery is net discharging (load exceeds solar input). In this example solar is producing 142 W, the load is drawing 225.5 W, and the battery covers the difference (~83.5 W).csof 5 is Float: the MPPT charge algorithm has reached its float stage, the battery is at working SoC, and solar is meeting its maintenance demand, but site loads still exceed current PV output. Whenttg_minis −1, the SmartShunt is present but the battery isn't in an active discharge (TTG is inapplicable or infinite). A positive value such as"ttg_min": 312means 5 h 12 minutes of estimated run time left. If no valid reading was available for a metric in the window, its field shows −9999 (float sentinel) or −1 (cs sentinel) instead of being left out. For example,"bat_temp_c": -9999.0means no temperature sensor is connected to the SmartShunt.solar_alert.qo: sent only on a threshold trip, withsync:truefor immediate delivery. There are four alert types, and the context values vary by alert. Here's an example low-SoC alert:Fields:{ "alert": "soc_low", "v1": 18.2, "v2": 24.1, "v3": -156.8 }v1= current SoC %,v2= battery voltage V,v3= net battery power W (negative = discharging). §8 has the complete alert taxonomy and what the context values mean for each alert type (temp_high,load_high,harvest_deficit).
7. Firmware Design
The firmware is split across six files:
firmware/solar_battery_controller/solar_battery_controller.inois the main application sketch (orchestration, accumulation, alert evaluation).firmware/solar_battery_controller/solar_battery_controller_helpers.his the VE.Direct parser header.firmware/solar_battery_controller/solar_battery_controller_helpers.cppis the VE.Direct parser implementation.firmware/solar_battery_controller/solar_battery_controller_notecard_helpers.hholds theAppStatestruct, all configuration constants, and the Notecard I/O declarations.firmware/solar_battery_controller/solar_battery_controller_notecard_helpers.cppimplements the Notecard I/O (hub.set, templates, env vars, Note queuing).firmware/solar_battery_controller/cx_sleep.hhandles host sleep: thecard.attnrequest, STOP2 entry, and ATTN-pin wake (cxSleepBegin,cxSleepUntilAttn).
7.1 Installing and flashing
Dependencies:
- Arduino core for STM32, from
stm32duino/Arduino_Core_STM32. Install it from the Arduino Boards Manager (search "STM32 MCU based boards"), or add the index URLhttps://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.jsonunder File → Preferences → Additional Boards Manager URLs. The Notecarrier CX's host uses the Cygnet board definition (see the build settings below). Blues Wireless Notecardlibrary (note-arduino). Install it from the Arduino Library Manager (arduino-cli lib install "Blues Wireless Notecard"). Before you start a new project, check the note-arduino releases for the latest stable version.STM32duino Low Powerlibrary (and its dependencySTM32duino RTC), fromstm32duino/STM32LowPower. Install it from the Library Manager. It 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 (the Notecarrier CX USB-C switch in NC, or
no cable), the STM32's USB wakeup interrupt fires continuously and the host
never stays in STOP2. Debug output goes to the LPUART on the Notecarrier CX
debug jack instead, which an ST-LINK V3 exposes as a virtual COM port (see
Serial Logging With STLINK).
Flashing with the Arduino IDE: Open solar_battery_controller.ino, apply
the build settings above, and click Upload. With the Notecarrier CX's DIP
switch in HST, the host's DFU bootloader is reachable over the USB-C port
(hold BOOT, tap RST). You can also upload over SWD with an ST-LINK on the
debug jack.
Flashing with arduino-cli:
# Confirm the board name for your installed core version
arduino-cli board listall | grep -i cygnet
# Compile and upload (replace the board options and port to match your system;
# usb=none disables USB CDC, which must be off for the host to sleep)
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/solar_battery_controller/
arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/cu.usbmodem* firmware/solar_battery_controller/NOTE-NBGLWX Skylo build flag: For the NOTE-NBGLWX, add -DSKYLO_BUILD to
the compiler flags. It turns on the Skylo satellite fallback (card.transport
with method cell-ntn) and makes a 1440-minute daily summary the default
from the very first wake. Leave it off for the NOTE-MBGLW, which has no
satellite radio. In the Arduino IDE the flag goes in File → Preferences →
Additional build options. With arduino-cli, pass
--build-property "compiler.cpp.extra_flags=-DSKYLO_BUILD". The Skylo
deployment guidance in §6 explains why.
# Example: Skylo build
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none \
--build-property "compiler.cpp.extra_flags=-DSKYLO_BUILD" \
firmware/solar_battery_controller/After uploading, open a serial monitor at 115200 baud on the ST-LINK virtual
COM port. You'll see the Notecard configuration log at power-up. After that,
each 15-minute wake prints a one-line [summary] or [alert] entry, and then
the output goes quiet as the host goes back into STOP2.
7.2 Modules
| Responsibility | Where |
|---|---|
Notecard configuration (hub.set, templates, accelerometer disable) | notecardFirstBoot, applyHubSetIfChanged, defineTemplates |
| Environment variable fetch and threshold refresh | fetchEnvOverrides |
| VE.Direct frame reading (two devices) | readVEDirectFrame in helpers |
| Sample accumulation into rolling window averages | accumulate |
| Alert evaluation and immediate-sync Notes | checkAndSendAlerts, checkHarvestDeficit, sendAlert |
| Summary Note construction and emission | sendSummary |
Host sleep (STOP2 until the Notecard raises ATTN) | cx_sleep.h: cxSleepBegin, cxSleepUntilAttn |
| Application state (held in RAM across sleep cycles) | AppState struct |
7.3 VE.Direct reading strategy
VE.Direct protocol primer. Victron's VE.Direct is a one-wire async text
protocol at 19200 baud 8N1. The device broadcasts one frame per second without
any polling or handshake. Each frame is a series of LABEL<tab>VALUE<CR><LF>
lines ending with a Checksum<tab><byte><CR><LF> line. The checksum byte makes
the sum of all bytes in the frame (including the Checksum line itself) come
out to zero modulo 256. On a healthy bus you get 3–4 complete frames in a
3-second window.
readVEDirectFrame() in the helpers file reads characters from the given
Stream object until it sees a complete, checksum-verified frame or the timeout
runs out (3 seconds by default). It pulls out only the fields that matter for
the SmartShunt or MPPT and quietly ignores every other label. The SmartShunt
uses the Notecarrier CX's UART (Serial1), and a SoftwareSerial instance on D9
handles the MPPT. The host reads them one after the other at wakeup.
Why not poll both simultaneously? VE.Direct is a one-way broadcast, and both devices are always transmitting, so the firmware just listens to one at a time. Reading them in sequence adds ~6 seconds of active time per wake cycle, which is negligible next to a 15-minute sleep interval.
7.4 Event payload design
solar_summary.qo uses a compact Note
template
on port 50, registered on the first wake. Notecard stores templated Notes as fixed-length
records instead of free-form JSON, which cuts wire size by roughly 3–5×. That
matters on a system that will run for years on a prepaid cellular SIM. Every
field in the template schema is always present in every Note body, with
explicit sentinel values when no valid samples were collected for that metric in
the window:
SUMMARY_SENTINEL_F = −9999.0: all float fields (bat_v,bat_a,bat_w,soc_pct,bat_temp_c,pv_v,pv_w,yield_kwh,load_w).SUMMARY_SENTINEL_TTG = −9999:ttg_minwhen no SmartShunt data arrived in the window. A value of −1 (separate from the sentinel) means the SmartShunt was present but the battery wasn't in an active discharge (TTG is inapplicable or infinite). Any value ≥ 0 is the active discharge estimate in minutes.SUMMARY_SENTINEL_CS = −1:cswhen no MPPT data arrived. Every valid VE.Direct CS code for the SmartSolar MPPT is non-negative (0, 2–5, 7, 245, 247, 252; see theVED_CS_*constants insolar_battery_controller_helpers.h), so −1 clearly means no MPPT data and can't collide with a real charge-state value.
This fixed schema follows the pattern Blues reference designs already use. The
firmware fills every field, and the sentinels are nonzero on purpose.
Templated Notefiles drop any field whose value is exactly 0 on the way to
Notehub
(omitempty),
so a field missing from a summary is a real zero (such as pv_w or cs at
night), and a sensor fault always shows up as a sentinel instead. At the start of each summary window, ttg_min resets to −1
(resetAccumulators sets state.ttg_min = -1) so a stale value from the
previous window can't leak forward. If a whole window produces no valid samples
from either device, sendSummary skips the Note entirely and opens a new
window, and no all-sentinel record goes out.
solar_alert.qo has its own compact template on port 51, so alerts can go out
over satellite too (see the Skylo guidance in §6). All four alert types share
one schema. The alert field names the failure condition, and v1, v2, v3
carry context values whose meaning depends on the alert type:
| Alert | v1 | v2 | v3 |
|---|---|---|---|
soc_low | current SoC % | battery voltage V | net battery power W |
temp_high | battery temp °C | battery voltage V | SoC % |
load_high | load draw W | SoC % | PV power W |
harvest_deficit | avg SoC % over window | consecutive windows without Float/Absorb | today's yield kWh |
The same omitempty rule applies here, so a context value of exactly 0 is
missing from the alert in Notehub. For example, a load_high at night arrives
without v3, because PV power is 0.
7.5 Low-power strategy
The site runs on batteries, so the power budget matters. The host MCU spends
almost all of its time in STM32 STOP2, drawing a few microamps with its RAM
intact. After each wake cycle, loop() calls cxSleepUntilAttn() (in
cx_sleep.h), which sends a
card.attn sleep
request for sample_interval_sec seconds, waits for the Notecard to pull ATTN
low, and enters STOP2. When the ATTN timer fires 15 minutes later (by default),
the Notecard raises ATTN, the rising edge on D5 wakes the host, and
execution picks up on the next line of loop(). The host resumes in place
instead of cold-booting, so the AppState struct just lives in RAM. Nothing
gets serialized to the Notecard, and setup() runs only once per power-up. The
firmware opens the two VE.Direct UARTs only for the read and closes them again
before sleeping. Otherwise SoftwareSerial's pin-change interrupt on D9 would
wake the host every time the MPPT broadcast a frame. If the sleep request fails
or ATTN never goes low (missing jumper), loop() waits out the interval awake
and tries again on the next cycle, so the sample cadence holds either way.
Notecard's own idle draw is ~8–18 µA between cellular sessions (per the Blues
low-power design
guide). Add the host
and board quiescent, and Mojo at the +VBAT rail sees about 16–44 µA between
wakes. The site's battery bus carries two more standing loads that Mojo can't
see. Each voltage-divider network (10 kΩ + 20 kΩ = 30 kΩ total) pulls ~167 µA
continuously from its Victron device's TX line, so the two dividers add ~334 µA,
drawn through the Victron devices instead of through +VBAT. The Traco TSR 1
DC-DC converter draws about 1–2 mA at its VIN input from the battery bus, which
is upstream of the 5 V output rail. To measure the total battery-bus draw, you
need a separate instrument (for example, a clamp meter or series shunt) on the
converter's VIN supply wire. §9 has the complete system current table. The
Notecard's outbound sync cadence (4 hours by default) controls when it wakes
the radio. With 15-minute sample intervals and a 4-hour transmit cadence, the
radio fires 6 times per day instead of 96.
Sampling and transmission run on separate cadences on purpose. The device
samples every 15 minutes but only opens a cellular session every 4 hours. Alert
Notes get around this with sync:true, which tells the Notecard to open a
session as soon as an alert is queued instead of waiting for the next 4-hour
window.
7.6 Retry and error handling
readVEDirectFrameenforces a 3-second per-device timeout. If a device doesn't respond (powered off, disconnected, connector fault), the function returnsfalseandaccumulateskips the matching fields. Metrics that come only from the missing device end up with0valid samples for that window, andsendSummaryemits the sentinel value (−9999 for floats, −1 forcs) for those fields. If SmartShunt data drops out for a whole window, you'll see −9999 onbat_v,soc_pct, and the rest insolar_summary.qo, which makes the sensor fault obvious in Notehub.#define ALERT_COOLDOWN_SAMPLES 2(~30 minutes at the default 15-min interval) controls how often a persistent fault re-alerts. Each alert fires immediately on the first trip, then repeats every 2 wakes (~30 minutes) while the condition holds. When the condition clears, the active flag and cooldown counter both reset, so the next trip fires immediately again. One alert every ~30 minutes per condition keeps on-call informed without burning through the satellite data quota.- A failed summary send doesn't open a new window. When
note.addforsolar_summary.qofails,samples_until_summarystays at0(it isn't reset). On the next wake the firmware retries the send before reading new VE.Direct data, so the closed window's averages never get mixed with fresh readings.resetAccumulators()and the window-open only happen once the Note is confirmed queued. - The environment variable clamp logic in
fetchEnvOverridesrejectssample_interval_secvalues outside 60–3600 andreport_interval_minvalues outside 15–1440, so an operator typo can't make the device unreachable.
7.7 Key code snippet 1: Note template registration
Templates turn each Note into a fixed-length on-wire record, and the
placeholder values set the wire type: 14.1 = 4-byte IEEE-754 float, 14 =
4-byte signed integer, 12 = 2-byte signed integer, and a non-numeric string
such as "-" = variable-length string. format: "compact" plus a port on
each template is what lets both Notefiles sync over Skylo NTN. defineTemplates()
sends each request through a small sendTemplate() helper that checks the
response for an err field, and it returns true only when both templates
register.
J *req = notecard.newRequest("note.template");
if (req) {
JAddStringToObject(req, "file", "solar_summary.qo");
JAddNumberToObject(req, "port", 50);
JAddStringToObject(req, "format", "compact");
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "bat_v", 14.1); // 4-byte float
JAddNumberToObject(body, "bat_a", 14.1);
JAddNumberToObject(body, "bat_w", 14.1);
JAddNumberToObject(body, "soc_pct", 14.1);
JAddNumberToObject(body, "bat_temp_c", 14.1);
JAddNumberToObject(body, "pv_v", 14.1);
JAddNumberToObject(body, "pv_w", 14.1);
JAddNumberToObject(body, "yield_kwh", 14.1);
JAddNumberToObject(body, "load_w", 14.1);
JAddNumberToObject(body, "ttg_min", 14); // 4-byte int
JAddNumberToObject(body, "cs", 12); // 2-byte int
}
bool ok = sendTemplate(req, "solar_summary.qo");
req = notecard.newRequest("note.template");
if (req) {
JAddStringToObject(req, "file", "solar_alert.qo");
JAddNumberToObject(req, "port", 51);
JAddStringToObject(req, "format", "compact");
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "alert", "-"); // variable-length string
JAddNumberToObject(body, "v1", 14.1);
JAddNumberToObject(body, "v2", 14.1);
JAddNumberToObject(body, "v3", 14.1);
}
ok = sendTemplate(req, "solar_alert.qo") && ok;
return ok;7.8 Key code snippet 2: immediate-sync alert
sync:true tells the Notecard to skip waiting for the next outbound window
and open a session right away, over Skylo if a SKYLO_BUILD device can't reach
cellular. checkAndSendAlerts() in the sketch calls sendAlert() with the
alert name and its three context values, for example
sendAlert("soc_low", shunt.soc_pct, shunt.bat_v, shunt.bat_w). It arms the
alert's cooldown only when sendAlert() returns true.
bool sendAlert(const char *alert, float v1, float v2, float v3) {
J *req = notecard.newRequest("note.add");
if (req) {
JAddStringToObject(req, "file", "solar_alert.qo");
JAddBoolToObject(req, "sync", true);
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "alert", alert);
JAddNumberToObject(body, "v1", v1);
JAddNumberToObject(body, "v2", v2);
JAddNumberToObject(body, "v3", v3);
J *rsp = notecard.requestAndResponse(req);
if (!rsp) {
dbgSerial.println(F("[warn] note.add alert: no response"));
} else {
const char *err = JGetString(rsp, "err");
bool ok = (!err || !*err);
if (!ok) {
dbgSerial.print(F("[warn] note.add alert error: "));
dbgSerial.println(err); // log before deleteResponse
}
notecard.deleteResponse(rsp);
if (ok) {
dbgSerial.print(F("[alert] ")); dbgSerial.println(alert);
return true;
}
}
}
dbgSerial.print(F("[error] note.add failed for alert: ")); dbgSerial.println(alert);
return false;
}7.9 Key code snippet 3: sleep between samples
cxSleepUntilAttn() asks the Notecard to hold ATTN low for exactly
state.sample_interval_sec seconds, confirms the pin has gone low, and puts the
STM32 into STOP2. Because the host stays alive, the firmware checks the result
of the card.attn request instead of treating it as fire-and-forget. When the
Notecard raises ATTN, loop() runs again with state exactly as it was left.
setup() arms the wake once with cxSleepBegin(), and the end of loop()
sleeps:
// Arm the ATTN wake: the Notecard raises ATTN (D5) to end each sleep.
cxSleepBegin();
// ...
state.wake_count++;
// -------------------------------------------------------------------------
// Sleep in STOP2 until the Notecard raises ATTN at the next sample interval
// -------------------------------------------------------------------------
if (!cxSleepUntilAttn(notecard, state.sample_interval_sec, NULL, &dbgSerial)) {
// The Notecard didn't take the sleep request, or ATTN never went low
// (check the ATTN -> D5 jumper). Keep the sample cadence and try again.
dbgSerial.println(F("[sleep] ATTN sleep failed — waiting out the interval awake"));
delay(state.sample_interval_sec * 1000UL);
}7.10 Key code snippet 4: VE.Direct load computation
The firmware works out load draw algebraically instead of measuring it
directly: load is solar input minus net battery power. bat_w is positive when
the battery is charging (solar exceeding load) and negative when it's
discharging (load exceeding solar). With pv_w = 300 and bat_w = +100, the
load is 200 W, and solar is charging the battery while it powers the load. With
pv_w = 100 and bat_w = −150, the load is 250 W, and solar and battery are
both powering it. accumulate() only averages non-negative results into the
window:
// Derived: load_w = pv_w − bat_w (positive bat_w = charging = less load)
if (shunt.valid && mppt.valid) {
float load = mppt.pv_w - shunt.bat_w;
if (load >= 0.0f) {
state.load_w_sum += load; state.load_w_cnt++;
}
}8. Data Flow
Every 15 minutes (default sample_interval_sec = 900) the firmware wakes, reads
both VE.Direct devices, and adds one data point to the current summary window:
- Collected per sample. Battery voltage (V), battery current (A, signed), net battery power (W, signed), SoC (%), battery temperature (°C, when sensor present), panel voltage (V), panel power (W), and computed load (W). Only the latest value is kept for daily yield (kWh), charge state (integer enum), and time-to-go (minutes).
- Transmitted.
solar_summary.qo: once perreport_interval_min(every 4 hours by default, which is 16 samples per window). The firmware fills every template field. Each averaged field carries the mean of its valid samples, or −9999 when that metric has no valid samples.yield_kwhandcscarry the latest value at window close, not an average, andcsis −1 when no MPPT data arrived.ttg_minis −9999 when no SmartShunt data arrived, −1 when the shunt was present but the battery wasn't discharging, and otherwise the active minutes-remaining estimate. If a whole window has no valid samples from either device, the Note is skipped entirely.solar_alert.qo: sent immediately on a threshold trip withsync:true. A ~30-minute per-alert cooldown deduplicates it.
- Routed. Notehub forwards
solar_alert.qoto real-time notification destinations (NOC dashboards, on-call paging, SMS gateway) andsolar_summary.qoto analytics or long-term storage. With separate Notefiles, each one gets its own route, urgency tier, and retention policy. - Alert triggers.
soc_low: SoC belowsoc_alert_pct. Fires once per ~30 minutes while SoC stays low. Context: current SoC, battery voltage, net battery power.temp_high: battery temperature abovebat_temp_max_c(only when a temperature sensor is wired to the SmartShunt). Context: current temp, battery voltage, SoC.load_high: computed load draw aboveload_alert_w. Fires once per ~30 minutes. Context: load W, SoC, current PV power.harvest_deficit: the MPPT hasn't reached a full-charge state (Floatcs=5, Absorptioncs=4, Equalizecs=7, or Auto Equalizecs=247) forharvest_deficit_daysconsecutive days. It's disabled by default, and you turn it on with the env var. Fires once per ~24 hours while the deficit persists. Context: average SoC % over the window, number of consecutive windows without a full-charge state, and today's accumulated solar yield kWh. This alert is the main sign of a recharge imbalance, whether that's a degraded array, persistent cloud cover, or load growth the system can no longer recover from overnight. The firmware doesn't count Equalize/Recondition as a deficit. Those cycles run after the battery has already reached Float, so seeing one confirms a healthy charge cycle finished.
9. Validation and Testing
Expected steady-state behavior. A healthy site with solar in bulk or float
mode sends one solar_summary.qo every 4 hours and no solar_alert.qo events.
During the day bat_a will be slightly positive (the battery is taking a
maintenance trickle), and at night it'll be slightly negative (the load is
drawing from the battery). On a well-sized system, soc_pct should be above 80%
by mid-morning.
Using Mojo to validate the power profile. Notecard's published standalone idle draw is ~8–18 µA @ 5V, and the low-power firmware design guide has the authoritative figures by SKU. The assembled monitor draws a little more at its +VBAT input, from the host and board quiescent. The voltage dividers and the DC-DC converter add more standing current on the battery bus, but none of it passes through +VBAT, so Mojo doesn't see it. The table below breaks out both figures so the Mojo trace makes sense:
NOTE-MBGLW path (LTE Cat-1 bis cellular)
| Phase | Notecard-only | Measured by Mojo at +VBAT |
|---|---|---|
| Between wakes (host in STOP2, waiting for ATTN) | ~8–18 µA (+ ~1–2 µA host) | ~16–44 µA (Notecard ~8–18 µA + host ~1–2 µA + board quiescent; the dividers' ~334 µA comes from the Victron TX lines, not +VBAT) |
| Host active (VE.Direct reads + I²C) | N/A | ~6–8 mA for ~6–8 seconds per wake |
| Notecard cellular session (LTE Cat-1 bis outbound) | ~250 mA avg, ≤2 A peak, for ~15–30 seconds | Same, since the radio dominates |
Mojo measurement boundary. Mojo sits between the Traco 5 V output and the Notecarrier CX +VBAT pin, so it only measures current on the 5 V output rail. The Traco TSR 1's no-load input quiescent (~1–2 mA) comes from the battery bus at the converter's VIN pin, upstream of the 5 V rail, where Mojo can't see it. To measure total battery-bus draw including converter quiescent, put a separate shunt or clamp meter on the converter's VIN supply wire.
The two always-energized 10 kΩ/20 kΩ divider networks (~5 V ÷ 30 kΩ = 167 µA each, × 2 = ~334 µA) don't show up in the Mojo trace, because the Victron TX lines drive them, not the +VBAT rail. They're still a standing load on the site's battery, by way of the Victron devices. If you need to lower the site's total standing draw, go after the dividers and the converter quiescent. Switching to a level-shifting stage that powers off would remove ~334 µA.
Here's a rough 24-hour Mojo estimate at default settings (6 cellular sessions/day, 96 host wakes/day). Six LTE Cat-1 bis radio sessions dominate at roughly 8–10 mAh, 96 host wakes add roughly 1 mAh, and the idle floor adds about 0.4–1 mAh (16–44 µA × 24 h). That's about 10–12 mAh per 24 hours measured by Mojo at the +VBAT rail. (Mojo doesn't measure the battery-bus draw from the voltage dividers, ~334 µA × 24 h ≈ 8 mAh, or from converter input quiescent, ~1–2 mA × 24 h ≈ 24–48 mAh at the battery bus voltage.) Check it once with Mojo at bench bring-up. If the daily Mojo total is more than 3× above ~12 mAh, usually either the host isn't sleeping (a continuous multi-mA rise above the ~44 µA floor) or the radio is struggling with marginal signal (correctly spaced sessions that each run 60+ seconds).
To use Mojo, splice it inline between the DC-DC converter's 5 V output and the Notecarrier CX +VBAT pin, connect its Qwiic output to the Notecarrier CX I²C header, and leave the unit running for 24 hours. A healthy LTE Cat-1 bis trace shows a flat ~16–44 µA baseline (the +VBAT system idle floor), brief blips to ~6–8 mA every 15 minutes (host wake), and one 15–30-second burst to ~250 mA every 4 hours (cellular session). If the trace sits above the ~44 µA floor and the rise doesn't line up with a host wake or radio burst, there's an unexpected active current path somewhere.
NOTE-NBGLWX path (Skylo NTN satellite + LTE-M)
The satellite path's power profile looks very different from cellular. A satellite session needs the GNSS receiver to confirm the device's position, and then it drives the NTN modem through an uplink/downlink sequence that usually runs 2–5 minutes, compared to 15–30 seconds for an LTE Cat-1 bis session. Each session takes longer and uses more energy, but the recommended satellite cadence is once a day instead of six sessions a day on LTE Cat-1 bis. Those six LTE Cat-1 bis sessions use roughly 8 mAh/day combined, and the single daily satellite session is estimated at ~3–12 mAh (the phase table has the per-phase breakdown). Either way, the continuous ~1–3 mA standing current floor dominates the whole-system daily budget. The same voltage dividers and DC-DC converter quiescent are on both paths, so the daily total is comparable to the LTE Cat-1 bis path, not a lot lower. The breakdown below covers only what Mojo sees at +VBAT, where the satellite session is the biggest item.
| Phase | Measured by Mojo at +VBAT |
|---|---|
| Between sessions (host in STOP2, waiting for ATTN) | ~16–44 µA (Notecard + host ~1–2 µA + board quiescent; the dividers' ~334 µA and the converter's ~1–2 mA input quiescent are on the battery bus where Mojo can't see them, the same boundary as the LTE Cat-1 bis path) |
| Host active (VE.Direct reads + I²C) | ~6–8 mA for ~6–8 seconds per wake (96 wakes/day) |
| GPS acquisition with fixed location (warm fix) | ~30 mA for ~60 seconds ≈ 0.5 mAh |
| NTN satellite session (transmit + receive) | ~250–500 mA peak during the NTN data burst (~20–45 seconds), then ~30–80 mA average over the rest of the session overhead, for 2–5 minutes total. Estimated at ~3–12 mAh/session (validate with Mojo). |
| LTE-M cellular fallback (when available) | Similar profile to the NOTE-MBGLW above |
Per-day Mojo energy estimate at the recommended daily satellite cadence (1
NTN session/day, report_interval_min = 1440):
- Notecard + board quiescent (24 h × ~16–44 µA): ~0.4–1 mAh
- 96 host wakes (~7 mA × ~7 seconds each): ~1.3 mAh
- GPS fix (warm, fixed location): ~0.5 mAh
- 1 NTN satellite session: ~3–12 mAh (a peak 250–500 mA burst for ~20–45 seconds plus session overhead; see the phase table)
The satellite session dominates, and the total comes to about 5–15 mAh/day measured by Mojo at the +VBAT rail. Mojo can't see the dividers (~334 µA × 24 h ≈ 8 mAh) or the converter's input-side quiescent (~1–2 mA at the battery bus for 24 h), which are additional battery-bus draw. Those two standing loads are the main levers for total battery-bus draw. For reference, Blues published data shows Starnote for Skylo at a 60-minute NTN sync cadence using ~27 mAh over 12 hours. That's a full-system measurement that includes all the idle time between sessions at that product's quiescent draw, not an isolated per-session energy figure. The NOTE-NBGLWX uses a similar NTN radio subsystem, so treat this as an order-of-magnitude sanity check, and validate the actual per-session energy with Mojo on the bench before field deployment. The NOTE-NBGLWX datasheet has SKU-specific power curves.
A healthy Skylo trace on Mojo shows a flat ~16–44 µA baseline (the +VBAT system idle floor) and brief blips to ~6–8 mA every 15 minutes (host wake). Once a day there's a multi-stage radio burst. First comes a ~30 mA plateau for ~60 seconds (GPS fix), then a 2–5 minute NTN session. Inside that session, a peak ~250–500 mA transmit burst (~20–45 seconds) shows up as a sharp spike within a lower-current envelope (~30–80 mA for the rest of the session). If the GPS acquisition stage takes minutes instead of seconds, the device probably isn't in fixed-location mode (see the Skylo deployment guidance above).
Mojo is a bench bring-up tool, not a production sensor. Once a firmware revision passes the trace check, deployed units don't need it.
A Notecarrier CX and Notecard, paired with two Victron VE.Direct devices, turn an off-grid power system nobody could see into one you can watch from anywhere. The device wakes every 15 minutes, reads battery SoC, current, temperature, and solar harvest in a few seconds, and goes back to sleep, building up a window of averages that goes to Notehub every four hours. A site that's going dark sends a warning Note over cellular before the battery drops too far to communicate, and that gives the operations team a fighting chance to dispatch before the outage. None of this needs site networking, IT coordination, or any infrastructure that wasn't already there. All it takes is a 5V supply and two short VE.Direct cables.
For truly remote sites where terrestrial cellular coverage is marginal or
absent, Notecard for Skylo
(NOTE-NBGLWX) drops into the same
M.2 carrier and uses the same API as Notecard Cell+WiFi. Built with
-DSKYLO_BUILD, it falls back to satellite on its own, and the compact
templates carry both summaries and alerts over that link (see §6).
The Skylo variant also needs both antennas mounted outdoors with an
unobstructed view of the sky, as covered in §4 and §5. The same pattern works
for a single off-grid cabin or a fleet of hundreds of remote towers, and Notehub
handles per-fleet thresholds and per-device overrides.
10. Troubleshooting and Common Issues
| Symptom | Likely Cause | What to Check |
|---|---|---|
No solar_summary.qo or _session.qo Notes appear in Notehub after first power-on | ProductUID is unset, the antenna is disconnected, or the Notecard has no network coverage | Confirm that the PRODUCT_UID macro in solar_battery_controller.ino matches the value on your Notehub project page. Check that the cellular antenna is connected to the MAIN u.FL pigtail (NOTE-MBGLW), or that both the satellite and GPS antennas are mounted outdoors with a clear view of the sky (NOTE-NBGLWX). Open the blues.dev In-Browser Terminal and send {"req":"hub.status"} to see the current connection state. |
solar_summary.qo Notes arrive but every metric is -9999 (the float sentinel) | VE.Direct frames aren't parsing because of a wiring fault, the wrong pin, or a checksum mismatch | Check the resistor divider on each RX line (10 kohm in series, 20 kohm to GND), and confirm the divider midpoint reaches the right CX header pin (RX for the SmartShunt, D9 for the MPPT). Use a multimeter to confirm that pin 1 on the JST PH connector is GND and pin 3 is the device TX, since wire colors aren't standardized. Watch the serial monitor at 115200 baud for the per-cycle [summary] line. A parse failure prints a checksum or timeout warning. |
soc_pct reads implausibly high or low (for example, stuck at 100 percent or jumping by tens of percent between samples) | The SmartShunt's internal SoC calibration hasn't converged yet, or the shunt configuration doesn't match the installed battery bank | Use the Victron VictronConnect mobile app to confirm that the SmartShunt's battery capacity (Ah), charged voltage, and Peukert exponent match the installed bank. Give SoC at least one full charge cycle to converge after the shunt is first installed. The shunt computes SoC itself, and the firmware only reports the value broadcast on the wire. |
pv_w and yield_kwh show zero throughout the daytime hours | The MPPT has no PV input, the MPPT VE.Direct cable isn't connected, or the SoftwareSerial RX pin is miswired | Make sure the solar array breaker is closed and the MPPT reports nonzero panel voltage in VictronConnect. Confirm that the MPPT VE.Direct cable lands on CX header D9 through its own divider, not on the SmartShunt UART (Serial1). A persistent cs value of -1 in summary Notes confirms no MPPT frames were parsed in the window. |
Cellular session repeatedly fails (_session.qo shows non-zero voltage errors or failure_reason populated) | Marginal LTE Cat-1 bis signal at the install location, antenna placement inside a metallic enclosure, or insufficient peak current at the +VBAT rail | Check the bars and rssi fields in _session.qo. For metallic enclosures, route the antenna outside the enclosure on a u.FL-to-SMA pigtail with an IP67 bulkhead. Confirm that the DC-DC converter can supply the 750 mA sustained draw the Notecard datasheet calls for (the TSR 1's 1 A rating covers it), and that the input fuse is sized to match. |
| Skylo satellite session times out, or no NTN session ever completes | GPS fix unavailable or antenna obstructed | Confirm that both the satellite and GNSS antennas are mounted outdoors, since satellite signals don't get through a metal enclosure. If you like, send {"req":"card.location.mode","mode":"fixed","lat":...,"lon":...} and then {"req":"ntn.gps","on":true} once at commissioning so the satellite module skips its own GPS fix each session. Allow 5 to 15 minutes on first power-on for the initial NTN registration. The satellite best practices guide has more. |
| Intermittent VE.Direct parse failures (some windows produce sentinels, others succeed) | Long cable runs, ground loops between the battery shunt and the Notecarrier, or excessive noise coupling onto the RX line | Keep VE.Direct cables under the supplied 1.8 meter length, don't run them parallel to high-current DC bus bars, and connect the battery-bus return to signal ground at one point only (not both at the converter and at the shunt). If runs can't be shortened, add a small ferrite bead on the RX line near the Notecarrier CX header. |
| Device goes silent after several days, never reappears in Notehub | Battery bank discharged below the converter's input cutoff, or the input fuse opened | The Traco TSR 1-2450 holds regulation down to 6.5 volts input, and the TSR 1-4850 down to 18 volts. Below that cutoff, the 5 volt rail collapses and the Notecarrier CX powers off. Once solar brings the bus back up, the device cold-starts and rejoins. Inspect the inline fuse and check that the battery bus is above the converter's minimum input. The last _session.qo timestamp tells you when the radio went silent. |
If you hit an issue that isn't covered above, the Blues community forum is the fastest way to a working answer. Blues engineers and other field deployers both keep an eye on it.
11. Limitations and Next Steps
This reference design stays narrow on purpose. It answers one site-uptime question, "is this off-grid site healthy enough that I don't need to send a truck?", and leaves the harder problems (cell-level diagnostics, MPPT control, multi-bank aggregation) to companion designs where they belong.
Simplified for the POC
Each of these was a deliberate scope choice. Together they mark where this site-uptime monitor stops and where a companion design or firmware extension would pick up.
Read-only over VE.Direct. The firmware listens to the broadcast text protocol but never writes a command back to either Victron device. You set SmartShunt or MPPT parameters (battery capacity, absorption voltage, equalization schedule) with the Victron VictronConnect app at commissioning, not from Notehub. Write support would mean handling the proprietary VE.Direct HEX framing on top of the text protocol covered here.
Single battery bank, single MPPT. The reference design assumes one SmartShunt watching one battery bank and one SmartSolar MPPT watching one solar array. Sites with parallel banks or multiple charge controllers need either a Victron Cerbo GX (which aggregates over Victron's CAN bus) or a firmware extension that opens more VE.Direct serial ports on the Cygnet host.
No MPPT control. The firmware reports what the MPPT is doing, but it can't command a different absorption voltage, force a manual equalize, or turn off charging. Remote control of the charge algorithm is a different kind of problem with safety implications, and it's out of scope on purpose.
Bank-level signal scope only. Per-cell voltages and cell imbalance aren't on the VE.Direct wire. As §1 points out, they travel over CAN bus and need dedicated hardware. This design targets bank-level uptime failure modes and leaves cell-level telemetry to a companion design.
Sensor selection is fixed at the firmware level. The two VE.Direct ports are hard-coded to a SmartShunt on Serial1 and an MPPT on the SoftwareSerial pin. To swap in a different VE.Direct device family (such as a Phoenix inverter), you'd have to extend the parser to recognize its label set.
No local sample history. The host keeps the current summary window in the
AppState struct in RAM between sleeps (STOP2 retains RAM), but a power
interruption or reset wipes the in-progress accumulator. Long-term storage lives
in Notehub, not on the device.
Production Next Steps
Getting this monitor ready for a production fleet means handling multi-bank sites, adding local alerting and field provisioning, and supporting over-the-air updates for devices that may go years between physical visits. These extensions are the natural next moves.
Multi-bank support. Open more SoftwareSerial ports on free host GPIO pins and parse a second SmartShunt and MPPT pair. Add a per-bank index to each Note so downstream analytics can attribute readings to the right bank.
Alarm output relays. Drive a host GPIO to a small SSR or low-side switch, so
a harvest_deficit or soc_low alert can trigger a local audible or visual
indicator at the site for technicians on the ground.
Weather-data overlay. Route solar_summary.qo events into a server-side
process that joins each window's yield_kwh against a weather feed (cloud
cover, GHI) for the site coordinates. A real recharge-deficit problem looks very
different from a stretch of cloudy weather, and the overlay tells the two apart
automatically.
BLE-based field provisioning. A Bluetooth provisioning page on the Notecarrier CX would let a technician set the site identifier, fixed GPS coordinates, and initial thresholds from a phone in the field, without a laptop or USB cable. Notecard supports BLE on supported SKUs.
Notecard Outboard DFU for fleet firmware updates. Notecard Outboard DFU lets Notehub push a new STM32 firmware image to every device in a fleet over the air. A device that may sit on a remote tower or backcountry array for years between physical visits really needs that.
Multi-phase or DC-coupled load shunting. Sites that include AC inverter
loads (Victron MultiPlus or similar) can add a Quattro or MultiPlus VE.Bus
integration to capture inverter input, output, and inverter-internal alarms.
That gives a fuller picture of total site load than the algebraic
pv_w - bat_w computation used here.
12. Summary
The on-call engineer responsible for a remote tower or backcountry array now
gets days of warning, which they never had before. A harvest_deficit Note
arrives in the NOC with enough context (consecutive deficit windows, average
SoC, today's yield) to send a truck before the bank empties, not after the site
has already gone dark. The Notecarrier CX, one Notecard, and the two Victron
VE.Direct devices already on the wall do all the work. The same firmware reaches
Notehub over cellular where it can, and on a Notecard for Skylo built with
-DSKYLO_BUILD, over Skylo NTN satellite where it can't.
The same pattern works anywhere a VE.Direct device, a Modbus-RTU sensor, or a simple analog signal has to reach the cloud from a site with no IT infrastructure of its own.
On This Page
- 1. Project Overview
- 2. System Architecture
- 3. Technical Summary
- 4. Hardware Requirements
- 5. Wiring and Assembly
- 6. Notehub Setup
- 7. Firmware Design
- 7.1 Installing and flashing
- 7.2 Modules
- 7.3 VE.Direct reading strategy
- 7.4 Event payload design
- 7.5 Low-power strategy
- 7.6 Retry and error handling
- 7.7 Key code snippet 1: Note template registration
- 7.8 Key code snippet 2: immediate-sync alert
- 7.9 Key code snippet 3: sleep between samples
- 7.10 Key code snippet 4: VE.Direct load computation
- 8. Data Flow
- 9. Validation and Testing
- 10. Troubleshooting and Common Issues
- 11. Limitations and Next Steps
- 12. Summary