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_rightSolar Array String-Level Performance Dashboard

Solar Array String-Level Performance Dashboard

Solar Array String-Level Performance Dashboard 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/solar_string_monitor/ so it can learn how the device works and help you adapt it to your hardware.

This project is an asset performance optimization reference design that keeps continuous watch on every individual string in a solar array. It catches soiling, shading, and bad modules wherever the array happens to be. You don't need WiFi or anything from site IT, and the existing inverter stays untouched.

1. Project Overview

The problem. Utility-scale and large commercial-and-industrial (C&I) solar installations routinely lose 3–8% of annual production to three mundane, entirely preventable causes. Dust, bird droppings, and pollen soil the panels. A branch grows six inches over winter, or a new HVAC unit goes in on a flat roof, and part of the array ends up shaded. Or a single module in a series string degrades or fails. Each of these leaves a distinctive electrical signature in the per-string DC current and voltage data, so none of the losses are invisible. The trouble is that almost nobody reads that data in real time.

A modern string inverter or combiner box already knows, down to the register, how much DC current and voltage each string is producing. It just has no network path off the array and into the asset manager's dashboard. To get the data, a maintenance tech has to show up on-site with a laptop and a Modbus cable, and that doesn't happen until something breaks badly enough to open a service ticket. Meanwhile, months of partial production loss pile up, and neither the owner nor the O&M (operations and maintenance) contractor sees them until the quarterly energy report shows a yield gap.

This project adds the missing network path. Every five minutes it reads per-string DC voltage and current from a Modbus RTU source, usually a multi-MPPT string inverter where each MPPT input tracks one string. It also reads irradiance from a pyranometer (a calibrated instrument that measures incident solar radiation in watts per square meter, W/m²) and panel temperature from a backsheet probe. From those, it computes a Performance Ratio (PR) for each string on its own. PR is the ratio of a string's actual power to the power you'd expect at the current irradiance and temperature.

Strings that fall below a configurable PR threshold get an immediate alert with a root-cause hypothesis (shading, soiling, or string fault). That hypothesis depends on an independent operating voltage reading for each string, which multi-MPPT inverters implementing the SunSpec Model 160 Multiple MPPT register model provide. §11 covers hardware compatibility and what changes when the monitored device only reports per-string current. When a string's PR recovers above the threshold, the device rearms the alert locally. It clears the internal underperformance flag so a later drop fires a fresh event, but it doesn't send a recovery Note to the Blues Notehub cloud service. Every hour, a summary Note records the per-string window means, plus the array-level irradiance and module temperature that all the strings share as a reference, for trend analysis.

Why Notecard. Solar arrays sit on rooftops, in open fields, and under parking canopies, which are about the worst places for WiFi range and the hardest places to get site IT involved. Nobody is going to install a permanent AP on the ballast tray of a strip-mall rooftop system. A ground mount in a rural field has no building nearby. And at a carport canopy over a retail parking lot, the network belongs to the tenant's coffee franchise, not the solar O&M contractor. Cellular gets around all of that. Notecard Cell+WiFi ships with a prepaid global SIM and registers on the cellular network by itself, so there's no SIM activation form, no per-site IT discussion, and no router credentials to manage.

The WiFi radio is there in the hardware, but this build doesn't use it. A metal NEMA 4X enclosure blocks much of the WiFi signal, and the firmware explicitly clears any stored WiFi credentials at first boot with card.wifi. That guarantees cellular-only operation even on a reused or previously provisioned Notecard, instead of assuming anything about the device's prior state. For a portfolio O&M operator managing dozens of customer sites, the biggest win with cellular-first IoT is that the same firmware and SKU deploy the same way on site one and site forty-three, with no site-specific network configuration at all.

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 metal NEMA 4X enclosure mounts on or near the combiner box or string inverter and takes power from the array's AC output through a compact AC/DC converter. Three cables leave the enclosure: a shielded RS-485 pair to the combiner's Modbus port, a pair for the pyranometer, and the DS18B20 backsheet temperature probe cable, which runs to a representative panel in the array. You don't modify the inverter, and the plant network never comes into it.

Before You Start — Critical Constraints

Per-string independent voltage required. This firmware reads a [V, I] register pair per string and classifies root causes by comparing each string's operating voltage and current to the fleet mean. Multi-MPPT inverters that implement SunSpec Model 160 Multiple MPPT expose per-MPPT voltage and current, and they work perfectly. Traditional string combiners put multiple strings on a shared DC bus and only expose per-string current, with no per-string voltage. Against a combiner-only source, the shading and string_fault root-cause classifications will never fire, because both need a low per-string voltage. See §11 Limitations for details.

Maximum 4 strings supported. The compile-time constant MAX_STRINGS = 4 fits the STM32L433's 64 KB SRAM budget. Going beyond 4 is untested and means revalidating the static memory footprint.

Modbus register map is a demo. The firmware reads contiguous holding-register pairs [V, I, V, I, …] with fixed scaling. Real inverters (SMA SunnyBoy, Fronius Symo, Huawei SUN2000, and others) each have their own register maps, addressing conventions, and scaling. In production you'll need a vendor-specific firmware build or careful manual register validation (see Modbus first-light in §9).

2. System Architecture

System architecture: inverter (Modbus RTU), pyranometer, and DS18B20 backsheet probe → Notecarrier CX with STM32 host and Notecard MBGLW → cellular → Notehub → O&M / yield / alerts

Device-side responsibilities. Every five minutes, the Notecard's ATTN pin wakes the Notecarrier CX's STM32L433 host from STOP2, and the host reads its three inputs in turn: per-string DC voltage and current over Modbus RTU, irradiance from the pyranometer's analog output, and module temperature from the 1-Wire probe. With those readings it computes a temperature-derated expected power for each string and turns that into a Performance Ratio it can check against the alert threshold locally. Either a fresh summary Note goes into the queue or, if a string has fallen below the threshold, an immediate alert fires. Then the host goes back to STOP2 (a few µA) until the next ATTN wake. All of it moves over I²C to the Notecard in the carrier's M.2 slot, so the host never touches modem AT commands, session management, or raw sockets.

Notecard responsibilities. The Notecard handles everything after that. It queues Notes in on-device flash, brings up cellular sessions on the hub.set outbound cadence (60 minutes by default), and wakes the radio right away for anything marked sync:true. Environment variables travel the other direction on the same channel, so an O&M operator can retune thresholds, register addresses, and scaling factors across a whole portfolio of devices from Notehub without anyone touching firmware.

Notehub responsibilities. The data lands in Notehub. Events arrive over the Internet, Notehub stores every one, and project-level routes forward them to wherever the operator's downstream system needs them. Summaries and alerts go into separate Notefiles, so each stream can go to its own destination at its own urgency without any filter logic. Smart Fleets let a multi-site O&M operator slice the portfolio, with one fleet per customer or one per panel technology (monocrystalline vs bifacial, for example). Each fleet carries its own environment-variable overrides, and the firmware never has to fork.

Routing to the cloud (high level only). Notehub can send data to HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations. Route setup is specific to each project, and this one doesn't include a downstream endpoint. The Notehub routing docs cover the details.

3. Technical Summary

What you'll have when you're done: A cellular-connected device that reads per-string DC voltage and current from your Modbus inverter or combiner every 5 minutes and computes each string's performance ratio (actual power / temperature-derated expected power). When any string drops below 80% of expected power, it fires an immediate alert to Notehub with a root-cause hypothesis (shading, soiling, or string fault). It also sends an hourly summary Note for trend analysis.

Fastest path to first event (30 minutes):

  1. Assemble the hardware (Notecarrier CX, Notecard Cell+WiFi, RS-485 transceiver, and sensors) and fit the ATTN→D6 sleep jumper (see §5 Wiring and Assembly).
  2. Set up your project in Notehub and claim the Notecard (§6 steps 1–2).
  3. Copy the firmware directory and edit solar_string_monitor.ino, replacing PRODUCT_UID with your Notehub project UID.
  4. Build and flash (see Build Instructions below).
  5. Power up the assembly. On first boot, the Notecard registers with your project on its own.
  6. Check Notehub. A solar_alert.qo event shows up within a minute or so of any sample that trips an alert, and the first solar_summary.qo arrives after the first hourly report window closes (12 samples by default).

Build Instructions:

arduino-cli core install STMicroelectronics:stm32
arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC" ModbusMaster OneWire DallasTemperature
arduino-cli compile --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/solar_string_monitor/solar_string_monitor.ino
arduino-cli upload -p /dev/ttyACM0 --fqbn STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/solar_string_monitor/solar_string_monitor.ino

The usb=none board option turns off the USB CDC stack, and it has to be off for the host to sleep (see §7). firmware/solar_string_monitor/sketch.yaml sets the same board options, so arduino-cli compile run from that directory without --fqbn picks them up too.

Here's a sample Note from this device:

{
  "file": "solar_alert.qo",
  "body": {
    "string_id": 3,
    "reason": "shading",
    "perf_ratio": 0.63,
    "actual_w": 2989.8,
    "expected_w": 4766.5,
    "string_v": 402.4,
    "string_a": 7.43,
    "irradiance_wm2": 882.1,
    "mod_temp_c": 53.4
  }
}

4. Hardware Requirements

PartQtyRationale
Notecarrier CX1Integrated carrier with an onboard STM32L433 host. It exposes the UART TX/RX and A0–A5 analog inputs this project needs, so you don't need an external MCU. The host sleeps in STOP2 between samples, and the Notecard's ATTN pin wakes it.
Notecard Cell+WiFi (MBGLW) (datasheet)1Cellular connectivity removes any dependence on per-site WiFi or IT. Prepaid SIM included.
Taoglas GA.111.101111 LTE Magnetic Mount Antenna, 3 dBi, 1 m SMA1Omnidirectional LTE/3G/2G antenna covering 698–2700 MHz. Its strong neodymium magnet base mounts directly on the metal NEMA 4X enclosure lid with no extra hardware, and the 1 m RG-174 cable ends in an SMA male connector. The antenna has to sit outside the metal enclosure for reliable LTE Cat-1 bis reception, because internal antennas next to inverter metalwork won't work reliably.
SparkFun WRL-18568 SMA to U.FL Cable, 150 mm1Pigtail that connects the Notecarrier CX U.FL cellular antenna port to the GA.111's SMA male connector inside the enclosure. The GA.111 cable comes in through a weatherproof cable gland, and 150 mm of pigtail is enough to reach from the M.2 slot area to the gland.
Blues Mojo1Coulomb counter on the power rail for checking the sleep/wake power profile on the bench.
SparkFun RS-485 Transceiver Breakout (BOB-10124)1Converts the host UART to a half-duplex differential RS-485 signal. The SP3485 chip runs on a single 3.3V supply and tolerates 5V logic. It connects UART TX/RX and a direction-control GPIO (D9) to the RS-485 A/B bus.
Apogee SP-110-SS Self-Powered Pyranometer1ISO 9060 Class C silicon-cell pyranometer with a 0–400 mV analog output (0.2 mV per W/m²). It's self-powered, so it needs no excitation voltage, and it's designed for solar resource monitoring and PV system performance analysis.
Adafruit Waterproof DS18B20 Temperature Sensor (Product 381)11-Wire digital probe in a stainless-steel housing, accurate to ±0.5 °C from −10 °C to +85 °C. It's rated to 125 °C, which covers panel backsheet temperatures in hot climates. Attach it to a representative panel backsheet with thermal adhesive tape.
4.7 kΩ resistor11-Wire pull-up from DS18B20 data line to 3V3.
120 Ω resistor2RS-485 bus termination at each end of the cable (one at the Notecarrier CX breakout, one at the combiner/inverter).
Shielded twisted pair, 22 AWG, RS-485-rated1 runA → A, B → B wire pairs, with the shield bonded to chassis earth at the combiner end only (single-ended bond). If the inverter needs a dedicated COM/signal-ground conductor, add a third wire alongside the A/B pair (see Wiring and Assembly). Length depends on where the enclosure goes.
AC/DC supply, 5V/2A (e.g. MeanWell IRM-10-5)1Derives 5V DC from the array's local AC supply. The IRM-10-5 is encapsulated, rated for wide-range AC input (85–264 VAC), and delivers up to 2A (10W). That leaves enough headroom for the Notecard Cell+WiFi's cellular burst current, the STM32 host, and the RS-485 transceiver all running at once.
Metal NEMA 4X enclosure, ~6×4×3″1Weather-rated metal housing for outdoor installation near the combiner or on a rooftop equipment pad. The magnetic-base antenna mount (Taoglas GA.111) needs a metal lid. A non-metal enclosure would need a different antenna setup, such as an external bracket or mast mount.

All Blues hardware ships with a prepaid SIM that includes 500 MB of data and 10 years of service, with no activation fees and no monthly commitment.

5. Wiring and Assembly

Wiring: BOB-10124 RS-485 transceiver to the host UART (DE/RE on D9); Apogee SP-110-SS pyranometer to A0; DS18B20 to D5 with 4.7 kΩ pull-up; ATTN → D6 jumper so the Notecard can wake the host from STOP2; Taoglas GA.111 LTE antenna via SMA-u.FL; 120/240 VAC → IRM-10-5 → Mojo → +VBAT

warning

⚠️ Electrical safety requires qualified personnel. Solar PV arrays and their combiner/inverter equipment carry hazardous DC voltages (typically 300–1000 V on the string bus) and AC mains. All installation, wiring, and commissioning work must be done by a licensed electrician or qualified PV installer, following:

  • Applicable national and local electrical codes (NEC Article 690, IEC 62548, or your jurisdiction's equivalent)
  • The inverter and combiner manufacturers' installation instructions
  • Lockout/tagout (LOTO) procedures before working near or on energized PV conductors

Don't work on wiring connected to the PV array, combiner, or AC supply until isolation is confirmed.

Inside the NEMA 4X enclosure, everything ties back to the Notecarrier CX and its dual 16-pin header. The Notecard Cell+WiFi seats in the M.2 slot and talks to the STM32 host over the onboard I²C bus, so you don't run any data wiring between them. The one jumper you do add is ATTN→D6, which lets the Notecard wake the host from sleep. During bench validation, the Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad as a power monitor. A Qwiic cable runs from one of the Mojo's Qwiic ports to the Notecarrier CX Qwiic connector, which gives the Notecard a data path to read Mojo's coulomb counter. The Mojo subsection below and §9 walk through the bench readout procedure.

RS-485 transceiver (SparkFun BOB-10124 → Modbus combiner):

  • TX (Notecarrier CX) → DI (BOB-10124): UART transmit from the host to the RS-485 driver input.
  • RX (Notecarrier CX) → RO (BOB-10124): RS-485 receiver output back to the host.
  • D9 (Notecarrier CX) → DE and /RE tied together (BOB-10124): Direction control, where HIGH = transmit and LOW = receive (half-duplex). Tie DE and /RE to the same net.
  • +3V3 (Notecarrier CX) → VCC (BOB-10124): The SP3485 runs on 3.3V, so don't connect it to 5V.
  • GND → GND (BOB-10124).
  • A (BOB-10124) → combiner/inverter A (+) terminal.
  • B (BOB-10124) → combiner/inverter B (−) terminal.
  • Cable shield → combiner/inverter chassis earth at the combiner end only. A single-ended shield bond prevents ground loops, so do not ground the shield at both ends.
  • RS-485 signal ground (COM). Some inverters need a dedicated third conductor, often labeled C, COM, or GND on the RS-485 port, on top of the cable shield bond. It gives the two RS-485 nodes a common voltage reference. Check the inverter's Modbus wiring diagram. If it shows a COM terminal, run a third wire from the BOB-10124 GND pin to that terminal alongside the A/B pair. Without it, the bus can work over short distances but tends to pick up noise errors on longer cable runs.
  • Put a 120 Ω resistor across A/B at the BOB-10124 end of the cable and a second 120 Ω resistor at the combiner's RS-485 port to terminate the RS-485 bus.

Pyranometer (Apogee SP-110-SS → A0):

  • SP-110-SS positive (white) wire → A0 (Notecarrier CX).
  • SP-110-SS negative (black) wire → GND.
  • Mount the pyranometer co-planar with the panels (same tilt and azimuth) within 2–3 meters of the array, away from anything that could shade it and out of the panels' shadow.

Module temperature probe (Adafruit DS18B20 → D5):

  • DS18B20 data (yellow) → D5 (Notecarrier CX).
  • DS18B20 power (red) → +3V3 (Notecarrier CX). The DS18B20 can run on parasitic power from the data line, but an external 3V3 supply removes the timing sensitivity.
  • DS18B20 ground (black) → GND.
  • Put a 4.7 kΩ resistor from D5 to +3V3. This 1-Wire pull-up is mandatory.
  • Attach the probe tip to the backsheet of a representative module with thermal adhesive tape, mid-string, away from frame edges and bypass diodes.

Cellular antenna (Taoglas GA.111 + SparkFun WRL-18568):

  • Seat the WRL-18568 U.FL end onto the Notecarrier CX U.FL cellular antenna port.
  • Pass the GA.111's 1 m cable through a weatherproof cable gland in the enclosure wall, then screw the GA.111 SMA male connector onto the WRL-18568 SMA female end inside the enclosure.
  • Set the GA.111 magnetic base on the outside of the metal enclosure lid, away from the inverter's switching power supply and transformer. Aim for at least 30 cm of separation. The neodymium magnet holds the antenna securely on the metal surface with no extra hardware.
  • Don't run the antenna coax parallel to AC power cables inside the enclosure. If they have to share a path, cross the AC conductors at 90°.

ATTN-to-D6 jumper (host sleep):

Fit a short jumper between the ATTN and D6 pins on the Notecarrier CX header, which is the same 16-pin header that holds D5 and D9. D5 is the usual choice for this jumper, but here it carries the DS18B20 and D9 carries the RS-485 direction pin, so the firmware uses D6 through CX_ATTN_PIN. The Notecard pulls ATTN low while the host sleeps and raises it when the next sample is due, and the rising edge on D6 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.

Power chain:

  • AC mains → MeanWell IRM-10-5 (5V/2A output) → Mojo BAT input → Mojo LOAD output → Notecarrier CX +VBAT pad.

Mojo data connection (bench validation only):

  • The power path above already puts Mojo inline on the 5V rail, so you don't need any extra power wiring.
  • Mojo Qwiic port → Notecarrier CX Qwiic connector (standard 100 mm Qwiic cable). The Notecard reads Mojo's coulomb counter / power monitor over this Qwiic connection and exposes live power-consumption data through card.power. You'll use this data path to validate the sleep/wake power profile on the bench.
  • Reading Mojo data. Power the assembly only through +VBAT, with the USB cable to the Notecarrier disconnected (§9 explains why USB has to be absent). Then open the In-Browser Terminal and send {"req":"card.power"} to query Mojo live. To turn on periodic energy logging to Notehub, send {"req":"card.power","minutes":60}. The Notecard then writes a power-consumption snapshot to _log.qo at that interval, and you can export those from Notehub as CSV for full-session energy analysis. The application firmware doesn't call card.power on each sample cycle. Mojo measurements are bench instrumentation only (see §11 Limitations).
  • Before field deployment: remove the Qwiic cable. Mojo itself stays on the bench, so the only power path in the enclosure is IRM-10-5 → Notecarrier CX +VBAT.

6. Notehub Setup

  1. Create a project. Sign up at notehub.io and create a project. Copy the ProductUID and paste it into firmware/solar_string_monitor/solar_string_monitor.ino as PRODUCT_UID.

  2. Claim the Notecard. Power up the unit, and the Notecard joins your project on its first cellular session.

  3. Create a Fleet per site. Fleets group devices for shared configuration and routing. One fleet per installation makes sense here, since every device on a site reads the same inverter model with the same register map and the same string rating. Smart Fleets can move a device into a different fleet automatically (a replacement unit with a different panel model, for example) without re-flashing.

  4. Set environment variables. Every variable below is optional, and the firmware default applies to anything you don't set. A value set in Notehub overrides the compile-time default on the device's next inbound sync, so operators can retune thresholds and register addresses across a whole fleet without touching the firmware.

    VariableDefaultPurpose
    sample_interval_sec300Seconds between Modbus + sensor samples.
    report_interval_min60Minutes between summary Notes.
    modbus_slave_id1Modbus server (slave) address of the string combiner or inverter.
    modbus_baud9600RS-485 bus baud rate. Must match the inverter configuration. Common values are 9600 and 19200. Use modbus_parity and modbus_stop_bits for the rest of the serial framing.
    modbus_parity"none"RS-485 framing parity: "none", "even", or "odd". Must match the inverter's Modbus port configuration. The default "none" gives 8N1 or 8N2, depending on modbus_stop_bits. Changes take effect on the next device wake, with no firmware reflash.
    modbus_stop_bits1RS-485 stop bits per character: 1 or 2. Most Modbus RTU devices use 1 stop bit, but some older meters and energy analyzers need 2. Changes take effect on the next device wake.
    n_strings4Number of strings to poll (1–4). A single-string deployment (n_strings = 1) still fires PR-threshold alerts, but the root-cause hypothesis always comes out as degraded, because shading/soiling/string_fault classification needs at least two peer strings to compare.
    reg_base1000-based holding-register address of string 1's voltage register. The firmware reads 2×n_strings contiguous registers: [V1, I1, V2, I2, …].
    string_v_scale_x10010Voltage scaling factor × 100. Raw register value × (this / 100) = volts. Default 10 → 0.10 V/count.
    string_a_scale_x100010Current scaling factor × 1000. Raw register value × (this / 1000) = amps. Default 10 → 0.010 A/count.
    string_stc_w6000Rated power per string at STC (W), used to compute expected power. The 6000 W default represents a 15-module × 400 W C&I string (~600 V, ~10 A at STC). Update it to match the actual installed string rating before or right after commissioning.
    perf_thresh_pct80Alert when PR < this value / 100. Default 80 → alert when PR < 0.80.
    irradiance_min_wm2100Minimum irradiance (W/m²) before performance gets evaluated. It suppresses false alerts at dawn, at dusk, and under overcast skies, when noise dominates.
    temp_coeff_per10000-35Module power temperature coefficient × 10 000. Default −35 → −0.0035 /°C (typical for mono-PERC silicon). Adjust it to the value on the actual module datasheet.
    alert_cooldown_sec1800Minimum seconds between repeat alerts for the same string (default 30 minutes). The firmware converts it to a sample count at runtime, so the wall-clock window stays the same even if sample_interval_sec changes.
    pyranometer_mv_per_wm2_x1000200Pyranometer sensitivity × 1000 in mV/(W/m²). Default 200 → 0.200 mV/(W/m²) (Apogee SP-110-SS nominal). Replace it with the value from the unit's calibration certificate to correct for unit-to-unit variation.

    Register-map Note. The reg_base and scaling defaults are only examples for a demo register layout. The firmware reads contiguous pairs [V1, I1, V2, I2, …], one voltage and one current register per string, and it needs the monitored device to provide an independent operating voltage for each string. Multi-MPPT inverters implementing SunSpec Model 160 Multiple MPPT expose per-MPPT voltage (DCV) and current (DCA) in this form, but traditional string combiners usually don't. For production, look up the actual register addresses, addressing convention (0-based wire-level vs. 1-based / Modicon "40001" notation), scaling, and signedness in the inverter's Modbus communication manual. See Limitations.

    Timing constraint. For each summary window to cover exactly the configured report period, report_interval_min × 60 should be an integer multiple of sample_interval_sec. The defaults work: 60 minutes and 300 seconds give 3600 / 300 = 12 exactly. If the numbers don't divide evenly, the firmware uses ceiling division for the window length, so the window runs one sample long and covers at least the full configured period instead of falling short. The n_samples field in each summary Note shows the actual sample count. With a non-divisible configuration, it'll be ⌈report_interval_min × 60 / sample_interval_sec⌉ instead of the exact quotient.

  5. Configure routes. Add one route for solar_alert.qo (real-time delivery to an O&M on-call system or CMMS) and one for solar_summary.qo (batched delivery to a long-term analytics store). With the two Notefiles split at the source, each can go to its own destination at its own urgency, and the routes don't need filtering logic.

7. Firmware Design

The firmware is split between a main sketch, firmware/solar_string_monitor/solar_string_monitor.ino, and a pair of helper files for the sensor and Notecard data path, firmware/solar_string_monitor/solar_string_monitor_helpers.cpp and firmware/solar_string_monitor/solar_string_monitor_helpers.h. There's also firmware/solar_string_monitor/cx_sleep.h, which holds the host-sleep mechanics (card.attn request, STOP2 entry, ATTN wake). Splitting things up keeps the orchestration in .ino readable and puts the Modbus, sensor, template, and sleep logic in their own files.

Dependencies (install them from the Arduino Library Manager or with arduino-cli lib install):

  • Arduino core for STM32 (stm32duino/Arduino_Core_STM32), which supports the Notecarrier CX's STM32L433 host.
  • Blues Wireless Notecard (note-arduino), the Notecard library for Arduino.
  • ModbusMaster by 4-20ma, a portable Modbus RTU client that works with any Arduino Stream-compatible serial port, including Serial1 on the STM32L433.
  • OneWire and DallasTemperature, which together drive the DS18B20 over 1-Wire.
  • STM32duino Low Power (and its dependency STM32duino RTC), which 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 (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).

Modules

ResponsibilityWhere
Notecard configuration (hub.set, accelerometer off, template definitions)setup() in .ino → defineTemplates() in helpers
Environment-variable fetch and clampfetchEnvVars() in helpers
RS-485 direction controlpreTransmission() / postTransmission() callbacks in .ino
Modbus serial framing (parity / stop-bits)serialConfigFromEnv() in helpers → Serial1.begin() in loop()
Pyranometer readreadIrradiance() in helpers
DS18B20 module temperature readreadModuleTemp() in helpers
Modbus RTU string pollreadStrings() in helpers
Temperature-derated expected powercomputeExpected() in helpers
Per-string running accumulatorsaccumulateWindow() in helpers
PR evaluation, hypothesis, and alert emissionevaluateAndAlert() in helpers
Hourly summary NotesendSummary() in helpers
Host sleep (STOP2 until the Notecard raises ATTN on D6)goToSleep() in .ino → cx_sleep.h: cxSleepBegin, cxSleepUntilAttn
Application state (held in RAM across sleep cycles)AppState (g_state)

Sensor reading strategy

Pyranometer (Apogee SP-110-SS). The sensor puts 0.2 mV per W/m² straight onto A0. The firmware averages 8 samples to cut ADC noise, converts counts to millivolts at the host's 3.3V / 12-bit reference, and divides by 0.2 to get W/m². At 1000 W/m² (STC) the sensor outputs about 200 mV, which lands at roughly 248 ADC counts. That's enough resolution for threshold comparisons. A 10× non-inverting op-amp amplifier between the sensor and A0 would raise the effective resolution to about 11 bits, and it's recommended for production deployments where precise PR calculation depends on good irradiance measurements.

DS18B20 module temperature. The firmware calls 1-Wire requestTemperatures() + getTempCByIndex(0) at 11-bit resolution (375 milliseconds conversion time). If the probe is disconnected (DEVICE_DISCONNECTED_C) or the value is out of range (< −40 °C or > 110 °C), the firmware returns a −9999 sentinel and emits a rate-limited temp_probe_fault alert Note (sync:true, at most once per report window). The DS18B20's 85 °C "power-on sentinel" is not part of the validity checks. requestTemperatures() blocks for the full conversion time, so the scratchpad always holds a real measurement before the read. Treating 85 °C as a fault would wrongly reject genuine backsheet readings near 85 °C, and that's a realistic value for a hot rooftop array in summer.

For any sample cycle where the probe returns the −9999 sentinel, the firmware skips PR evaluation and per-string window accumulation, so no made-up expected-power values reach the accumulators or Notehub. In the summary, mod_temp_c emits −9999 for any window with no valid temperature readings, which lets downstream consumers tell a sensor failure apart from a real near-zero temperature measurement.

Modbus string reads. One readHoldingRegisters(base, 2×n) call pulls every string's voltage and current registers in a single Modbus transaction, which uses the bus roughly 8× more efficiently than reading each register separately. The firmware expects the registers in contiguous pairs [V1, I1, V2, I2, …] starting at reg_base. Real inverters vary, as Limitations explains.

Event payload design

The device uses two template-backed Notefiles. Templates store Notes as fixed-length binary records instead of free-form JSON, which cuts per-Note wire size by 3–5×. Over a multi-year deployment on a shared SIM, at 24 summary Notes/day per device, that saving matters.

Alert Note (solar_alert.qo, sync:true, fires when a Performance Ratio threshold trips):

{
  "file": "solar_alert.qo",
  "body": {
    "string_id": 3,
    "reason": "shading",
    "perf_ratio": 0.63,
    "actual_w": 2989.8,
    "expected_w": 4766.5,
    "string_v": 402.4,
    "string_a": 7.43,
    "irradiance_wm2": 882.1,
    "mod_temp_c": 53.4
  },
  "sync": true
}

Field reference:

  • string_id: 1–4 for the string that underperformed, or 0 for a Modbus bus failure
  • reason: "shading" (low V, normal I), "soiling" (normal V, low I), "string_fault" (both low), "degraded" (unclear pattern or single-string mode), "temp_probe_fault", "modbus_fail"
  • perf_ratio: actual power / temperature-derated expected power
  • actual_w: measured string power at that moment
  • expected_w: the derated STC expectation, calculated as STC power × (irradiance / 1000) × (1 + temp_coeff × (temp − 25))
  • Rate limiting: repeat alerts for the same string are suppressed for 30 minutes (configurable with alert_cooldown_sec)

Summary Note (solar_summary.qo, sent hourly):

{
  "file": "solar_summary.qo",
  "body": {
    "irradiance_wm2": 867.2,
    "mod_temp_c": 52.1,
    "s1_v": 596.4, "s1_a": 8.03, "s1_w": 4789.1, "s1_ew": 4710.8, "s1_pr": 1.02,
    "s2_v": 598.1, "s2_a": 8.01, "s2_w": 4790.8, "s2_ew": 4710.8, "s2_pr": 1.02,
    "s3_v": 401.3, "s3_a": 7.41, "s3_w": 2973.6, "s3_ew": 4710.8, "s3_pr": 0.63,
    "s4_v": 597.8, "s4_a": 8.02, "s4_w": 4794.4, "s4_ew": 4710.8, "s4_pr": 1.02,
    "alert_flags": 4,
    "n_samples": 12
  }
}

Field reference:

  • irradiance_wm2: window mean across all sample cycles (including Modbus failures), so the environmental context is always complete
  • mod_temp_c: window mean over the cycles where the DS18B20 returned a valid reading, or −9999 if there were none
  • sN_v, sN_a: voltage and current means over the samples where both the Modbus read and the temperature probe were valid, or −9999 if there were none (or the string isn't configured)
  • sN_w: actual power mean (V × I)
  • sN_ew: temperature-derated expected power mean
  • sN_pr: the window's performance ratio (sN_w / sN_ew)
  • alert_flags: bitmask (bit 0 = string 1, bit 1 = string 2, and so on). For example, 4 is binary 0100, meaning string 3 is currently flagged
  • n_samples: the number of sample cycles in the window, 12 by default (60 minutes ÷ 5 minutes). If report_interval_min × 60 isn't evenly divisible by sample_interval_sec, the window runs one sample longer (ceiling division)

Low-power strategy

The host wakes up and runs one complete sample cycle, which usually takes 2–5 seconds of active computation and I²C traffic. Then it calls goToSleep(). That function uses cxSleepUntilAttn() (in cx_sleep.h) to send a card.attn sleep request for g_sample_interval_sec seconds, wait for the Notecard to pull ATTN low, and put the STM32L433 into STOP2, where it draws a few microamps and keeps its RAM. When the interval is up, the Notecard raises ATTN. The rising edge on D6 wakes the host, execution picks up right where it slept, and loop() starts its next cycle. Since the host resumes in place instead of cold-booting, g_state just stays in RAM. 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), loop() waits out the interval awake and tries again on the next cycle, so the sample cadence holds either way.

The Notecard itself idles at ~8–18 µA between cellular syncs. The +3V3 rail stays up while the host sleeps, though, so the RS-485 transceiver's quiescent draw sits on top of that floor. If a lower idle floor matters, gate the transceiver's VCC from a spare GPIO. Sampling and transmission run on separate cadences on purpose. The device samples every 5 minutes but only connects to Notehub once an hour, and alerts are the only thing that skip the hourly transmit timer.

Sync strategy

The Notecard runs in hub.set periodic mode, so it wakes the radio on a schedule instead of holding a cellular connection open. outbound is set to g_report_interval_min (60 minutes by default), and queued summary Notes go to Notehub roughly once an hour. inbound is fixed at 120 minutes, which means the Notecard checks Notehub for fleet-level environment variable updates every two hours. A threshold or register-map change you publish in Notehub can take up to two hours to reach a deployed device, so plan for that lag when you tune a running installation. If you change report_interval_min through a Notehub environment variable, the firmware re-issues hub.set on the next wake cycle. That keeps the Notecard's outbound sync cadence aligned with the new summary period without a firmware reflash.

Alert Notes carry sync:true, which skips the scheduled outbound window entirely. The Notecard wakes the radio as soon as it gets a sync:true Note, whatever the outbound timer says. Alerts reach Notehub in roughly 15–60 seconds, and the bulk of the traffic still goes out in the efficient hourly batch. Connecting every 5 minutes for each summary sample would burn far more energy than collecting 12 samples and sending them in one session.

Retry and error handling

  • The first Notecard transaction in setup() is the cold-boot hub.set. If it fails, it returns false, the firmware logs the error, and last_hub_outbound stays at 0. The cadence-mismatch check at the top of loop() (which uses requestAndResponse and inspects the Notecard err field) then re-issues hub.set on the first pass and on every wake until it succeeds, so no device gets silently left unconfigured.
  • readStrings() tries the Modbus read up to 3 times before giving up. On a complete failure it emits a modbus_fail alert Note (rate-limited to once per report window) instead of quietly dropping the sample, so the O&M operator can tell "the array is down" apart from "the monitoring device lost Modbus".
  • fetchEnvVars() clamps environment variable values from Notehub to physically reasonable ranges, so a typo in the Notehub UI can't push g_perf_threshold to a value that fires on every sample or never fires at all.
  • Alert de-duplication uses a configurable alert_cooldown_sec window (1800 seconds, or 30 minutes, by default). The firmware computes the matching sample count at runtime as ⌈alert_cooldown_sec / sample_interval_sec⌉, so the 30-minute wall-clock window holds even if sample_interval_sec changes through a Notehub env var.

Key code snippet 1: Performance Ratio and root-cause hypothesis

The firmware computes PR for each string, then compares the operating voltage and current at the inverter/combiner measurement point against the fleet mean to pick a hypothesis. These are live operating-point readings under load, not open-circuit or short-circuit values. Low operating voltage with near-normal operating current points to partial shading. Near-normal voltage with low operating current points to soiling. When both are low, it's likely a more serious string fault. A string at or above g_perf_threshold (0.80 by default) just has its alert_active flag cleared, which rearms its alert.

Comparing strings takes at least two of them. With n_strings = 1 there's no fleet mean to compare against (the mean is just the single string's own values), so the firmware skips the hypothesis block and every PR alert fires as degraded. §11 Limitations has more on single-string and mixed-geometry deployments.

for (uint8_t i = 0; i < n; i++)
    pr[i] = (exp_w > 0.1f) ? (v[i] * a[i]) / exp_w : 0.0f;

for (uint8_t i = 0; i < n; i++) {
    if (pr[i] >= g_perf_threshold) { g_state.alert_active[i] = false; continue; }

    // Default hypothesis: "degraded" (PR below threshold, cause unclear).
    // Comparative shading/soiling/string_fault classification requires at
    // least two peer strings; with n == 1 both ratios equal 1.0 and the
    // block below is skipped, keeping the reason as "degraded".
    const char *reason = "degraded";
    if (n >= 2) {
        float vr = (v_mean > 0.1f) ? v[i] / v_mean : 1.0f;
        float ar = (a_mean > 0.1f) ? a[i] / a_mean : 1.0f;
        if      (vr < 0.85f && ar >= 0.90f) reason = "shading";
        else if (ar < 0.85f && vr >= 0.90f) reason = "soiling";
        else if (vr < 0.85f && ar <  0.85f) reason = "string_fault";
    }
    // ...
}

Key code snippet 2: temperature-derated expected power model

The expected power accounts for the gap between the current module temperature and the STC reference of 25 °C. A mono-PERC silicon module at 65 °C (common on a rooftop in summer) loses roughly 14% of its STC rating from temperature alone, and the model corrects for that before comparing actual power to expected. The firmware also floors the temperature factor at 0.40 as a guard.

float temp_factor = 1.0f + g_temp_coeff * (mod_temp_c - 25.0f);
if (temp_factor < 0.40f) temp_factor = 0.40f;
return g_string_stc_w * (irr_wm2 / 1000.0f) * temp_factor;

Key code snippet 3: immediate-sync alert

sync:true skips the periodic outbound window. The Notecard wakes the radio right away, and an alert typically reaches Notehub 15–60 seconds after the event. sendAlert() returns false if the Notecard doesn't answer or returns an error, and evaluateAndAlert() only starts the string's cooldown when it returns true, so a failed alert is tried again on the next sample.

bool sendAlert(uint8_t str_id, const char *reason,
               float pr, float v, float a, float irr, float mod_temp) {
    J *req = notecard.newRequest("note.add");
    JAddStringToObject(req, "file", ALERT_NOTEFILE);
    JAddBoolToObject(req,   "sync", true);
    J *b = JAddObjectToObject(req, "body");
    JAddNumberToObject(b, "string_id",      str_id);
    JAddStringToObject(b, "reason",         reason);
    JAddNumberToObject(b, "perf_ratio",     pr);
    JAddNumberToObject(b, "actual_w",       v * a);
    JAddNumberToObject(b, "expected_w",     computeExpected(irr, mod_temp));
    // ...
    J *rsp = notecard.requestAndResponse(req);
    if (!rsp) {
        debugSerial.println(F("[app] Alert: no response from Notecard"));
        return false;
    }
    const char *err = JGetString(rsp, "err");
    if (err && *err) {
        // ...
        return false;
    }
    notecard.deleteResponse(rsp);
    // ...
    return true;
}

Key code snippet 4: sleeping until the Notecard raises ATTN

cxSleepUntilAttn() asks the Notecard to hold ATTN low for g_sample_interval_sec seconds, confirms the pin has gone low, and puts the STM32 into STOP2. Because the host stays alive, the card.attn call is a request whose result gets checked, not a fire-and-forget command. Everything in g_state (sample count, accumulators, and alert state) stays in RAM, right where loop() left it when the host resumes. The DS18B20 has D5 and the RS-485 direction pin has D9, so the sketch defines CX_ATTN_PIN as D6 before including cx_sleep.h.

// D5 is the DS18B20 data line and D9 the RS-485 DE/RE pin, so the ATTN
// jumper lands on D6.
#define CX_ATTN_PIN D6
#include "cx_sleep.h"
// ...
static void goToSleep(uint32_t seconds) {
    if (!cxSleepUntilAttn(notecard, seconds, NULL, &debugSerial)) {
        // The Notecard didn't take the sleep request, or ATTN never went low
        // (check the ATTN -> D6 jumper). Keep the sample cadence and retry.
        debugSerial.println(F("[app] ATTN sleep failed — waiting out the interval awake"));
        delay(seconds * 1000UL);
    }
}
// ...
void setup() {
    // ...
    // Wake from STOP2 on the ATTN rising edge (ATTN jumpered to D6).
    cxSleepBegin();
}
// ...
void loop() {
    // ...
    // Sleep until the Notecard raises ATTN g_sample_interval_sec seconds from
    // now. Execution resumes at the top of loop(); g_state stays in RAM.
    goToSleep(g_sample_interval_sec);
}

8. Data Flow

Data flow: 5-min sample of per-string V/I plus irradiance and module temp → PR computation with root-cause classification (shading, soiling, string_fault) → solar_alert.qo (sync:true with 30-min cooldown) and solar_summary.qo (hourly templated) → Notehub routes

Collected every sample_interval_sec (default 5 minutes): DC voltage and current for each configured string (from Modbus), irradiance in W/m² (from the pyranometer ADC), and module backsheet temperature in °C (from the DS18B20).

Computed locally: expected power per string (irradiance-normalized and temperature-derated), performance ratio per string, and a root-cause hypothesis for any string below the threshold.

Transmitted:

  • solar_summary.qo: once per report_interval_min (24 Notes/day by default), template-encoded, then queued and shipped on the Notecard's outbound cellular sync. irradiance_wm2 is the window mean across all sample cycles, mod_temp_c is the mean over cycles with a valid probe reading, and the per-string fields are means over the window's samples where both Modbus and the probe were valid. A string with no such samples in the window emits −9999. You can tune sample_interval_sec and report_interval_min independently, but for the window to cover exactly the configured report period, report_interval_min × 60 has to be an integer multiple of sample_interval_sec. If it isn't, the firmware rounds the window length up (see the Timing constraint Note in §6).
  • solar_alert.qo: sent immediately with sync:true when a threshold trips, with a per-string de-duplication window (30 minutes by default, tunable with alert_cooldown_sec). A modbus_fail alert (string_id=0) fires when the RS-485 bus is unreachable and is rate-limited to once per report window. The alert_flags bitmask in solar_summary.qo reflects the last known active state of each string. The firmware clears the flags unconditionally whenever irradiance drops below irradiance_min_wm2. That happens at the loop level, regardless of whether Modbus or the probe succeeded, so overnight and low-light summaries always report 0 instead of carrying stale daytime fault states forward, even if telemetry happened to drop out at sunset. When irradiance is above the threshold, flags only update on sample cycles where Modbus polling succeeds and the temperature probe is valid. If telemetry drops out for several cycles within a daytime window, the flags carry forward the state from the most recent successful poll.

Routed. Both Notefiles reach Notehub, and the project's routes send them on to whatever downstream systems they specify. An O&M operator usually wants solar_alert.qo on their existing on-call channel (SMS, CMMS ticket, Slack) and solar_summary.qo in a long-term store for energy yield analysis.

Alert triggers:

note

Single-string Note. When n_strings = 1, comparative root-cause classification (shading, soiling, string_fault) isn't available, because there are no peer strings to compare against. Every PR-threshold trip fires as degraded. That's the correct, documented behavior (see §11 Limitations).

  • shading: string PR below the threshold, with operating voltage well below the fleet mean but operating current near normal. That's the classic partial-shade signature. A shadow across one or more cells pulls down the operating string voltage, while current in the bypass-diode conduction path is less affected. Requires n_strings ≥ 2.
  • soiling: string PR below the threshold, with operating current well below the fleet mean but voltage near normal. Uniform soiling (dust, pollen, bird droppings) cuts operating current proportionally across every cell in the string, and voltage drops only slightly. Requires n_strings ≥ 2.
  • string_fault: both voltage and current well below the fleet mean. This points to a serious fault, such as a broken cell, a failed bypass diode, high series resistance, or a physical disconnection. Requires n_strings ≥ 2.
  • degraded: PR below the threshold, but the V/I signature doesn't clearly fit any of the patterns above. It's the catch-all for mixed or unclear signatures, and the only hypothesis emitted when n_strings = 1.
  • temp_probe_fault: the DS18B20 backsheet probe returned an invalid reading (disconnected or out-of-range, i.e. < −40 °C or > 110 °C). It's rate-limited to once per report window. PR evaluation is skipped for the affected sample cycle, and mod_temp_c emits −9999 in any summary window with no valid temperature readings.
  • modbus_fail: the Modbus bus was unreachable after 3 attempts, so suspect the whole combiner or the RS-485 cable.

9. Validation and Testing

Expected cadence. On a sunny day in steady state, a healthy array produces one solar_summary.qo event every report_interval_min minutes (one per hour by default) and no solar_alert.qo events. If you turn report_interval_min down in Notehub, the firmware re-issues hub.set on the next wake cycle so the Notecard's outbound sync cadence keeps up with the new summary interval. Whenever you change report_interval_min or sample_interval_sec, make sure report_interval_min × 60 is still an integer multiple of sample_interval_sec. If it isn't, the firmware rounds the window length up so the summary window covers at least the full configured period (see the Timing constraint Note in §6). At night or under heavy overcast, irradiance falls below irradiance_min_wm2 and PR evaluation is suppressed, but the summary Note still goes out with the actual (near-zero) measured values.

Modbus first-light. Before you connect to the real inverter, run the firmware against a Modbus simulator (Modbus Mechanic, ModRSsim2, or any slave simulator) through a USB-RS-485 adapter, and check that the register reads match the expected values. Confirm that modbus_baud, modbus_slave_id, modbus_parity, and modbus_stop_bits match the inverter configuration. You can set all four through Notehub environment variables without re-flashing. The default framing is 8N1 (no parity, 1 stop bit), which matches most Modbus RTU devices. When field commissioning goes wrong, it's usually a single mismatched parameter.

Alert simulation. The easiest way to force alerts without climbing onto the roof is to set string_stc_w in the Fleet environment variables to at least four times the actual string's STC power rating (for example, change the default 6000 to 24000). The inflated expected-power denominator drives every configured string's computed PR well below the 80% default threshold. Once the next inbound sync delivers the change (which can take up to 120 minutes, as the Sync strategy section in §7 explains), all strings fire degraded alerts on their next sample. Confirm that solar_alert.qo events show up in Notehub for each configured string, then set string_stc_w back to the correct rated value.

warning

Don't use perf_thresh_pct values above 100 for this test. The firmware clamps that variable to a maximum of 100 (effective threshold: PR < 1.00), so setting 120 does nothing more than 100 would.

Using Mojo to validate the power profile. The table below separates Notecard-only figures from the published datasheet from the whole-assembly behavior you'll see at the +VBAT pad. A +VBAT measurement includes the Notecarrier regulator, the transceiver, and any peripheral quiescent draw on top of the Notecard itself, so it won't match the bare-Notecard datasheet values. Use the Mojo to characterize the real idle and burst figures for your own stack.

StateNotecard MBGLW (datasheet)Whole-assembly at +VBAT
Idle: host in STOP2, Notecard between syncs~8–18 µA @ 5V, the MBGLW datasheet idle figure (the range reflects variation across MBGLW hardware revisions)Notecard-dominated during sleep, with the STM32L433 in STOP2 adding ~1–2 µA. The +3V3 rail stays up, so the RS-485 transceiver's quiescent current (and the DS18B20's ~1 µA standby) sit on top of that, along with the Notecarrier regulator overhead. Measure your own stack with the Mojo to get the actual baseline.
Host awake + sampling (~2–5 s burst, every 5 min)Radio off. The Notecard is busy servicing I²C traffic from the host during this phase. There's no published datasheet figure for this transient state, so measure whole-system current at +VBAT with the Mojo.The STM32 core, RS-485 driver, pyranometer ADC, and 1-Wire transceiver all draw current during this burst. The whole-assembly +VBAT burst current is host-dominated and will be far above the bare-Notecard sleep floor. Measure it with the Mojo to characterize the actual burst current for your stack.
Cellular sync (hourly, ~15–60 s per session)100–300 mA average, with peak bursts during radio warm-up, network registration, and the TLS handshake. The MBGLW datasheet and the low-power firmware design guide have characterized per-session energy figures.Notecard-dominated, since the host is in STOP2 during the sync window

Wire Mojo inline in the power path (MeanWell 5V output → Mojo BAT, Mojo LOAD → Notecarrier CX +VBAT), and run a Qwiic cable from Mojo to the Notecarrier CX Qwiic port so the Notecard can read the coulomb counter. Disconnect USB from the Notecarrier, which is mandatory for reasons covered below. Then open the In-Browser Terminal and send {"req":"card.power"} to query Mojo live. For periodic energy logging, send {"req":"card.power","minutes":60}. The Notecard writes a power-consumption snapshot to _log.qo at that interval, and you can export those from Notehub as CSV for full-session energy analysis.

With the default cadence (5-minute samples, hourly sync), a healthy trace shows three distinct current levels. The lowest is the idle floor between sample cycles. It's Notecard-dominated, but the +VBAT reading will sit above the bare-Notecard ~8–18 µA figure because of the host's ~1–2 µA in STOP2, the RS-485 transceiver's quiescent draw on the always-on +3V3 rail, and the Notecarrier regulator overhead, so measure your own stack to establish the actual baseline. Every five minutes there's a brief 2–5 s active burst while the host wakes, reads sensors, computes PR, hands off to the Notecard, and goes back to sleep. Once an hour there's a ~15–60 s cellular excursion for the outbound sync, in the range the MBGLW datasheet characterizes. Alert Notes (sync:true) add an out-of-schedule cellular burst whenever a PR threshold trips.

Measurement conditions for sleep-current validation. To get close to the low-idle regime in the MBGLW datasheet, you have to measure with USB/VUSB absent. If VUSB is present on the Notecarrier, it holds the Notecard out of its lowest-power state no matter what the firmware does. Power the assembly only through the Mojo on the +VBAT pad, with no USB cable attached to the Notecarrier. The measured +VBAT idle floor will still sit above the bare-Notecard datasheet figure by the Notecarrier regulator overhead, the host's ~1–2 µA in STOP2, and the RS-485 transceiver's quiescent draw, and that's normal. If the idle floor stays above a few mA after you remove USB, the host isn't entering STOP2. Check the ATTN→D6 jumper and confirm the firmware was built with USB support set to None (usb=none). The debug console on the ST-LINK port prints [app] ATTN sleep failed when ATTN never reaches D6. As a secondary check, make sure the Notecarrier CX DIP switch is set to HST rather than NC. HST is the correct operating position, and it removes one possible source of spurious UART activity on the Notecard's diagnostic port that can get in the way of diagnosing sleep-path issues.

10. Troubleshooting

SymptomLikely CauseSolution
Notecard doesn't connect to cellular on first power-up.No ProductUID set, or invalid UID.Make sure PRODUCT_UID in the sketch is your real Notehub project UID, not the placeholder. Flash again and watch the debug console (the LPUART on the Notecarrier CX debug jack, read through an ST-LINK as described in §7) for error messages.
No solar_summary.qo events appear in Notehub.Device isn't claiming to the project, or the Notecard is in the wrong project.Open the In-Browser Terminal and send {"req":"hub.status"} to check which project the Notecard has claimed. If it's wrong, try {"req":"hub.set","product":"your-real-product-uid"} to re-claim.
solar_summary.qo events appear, but all strings report −9999 (no data).Modbus read failing, or the DS18B20 probe is faulted. Per-string fields only accumulate on samples where both are valid.Look for a modbus_fail or temp_probe_fault alert in Notehub. For Modbus, check that modbus_baud, modbus_slave_id, modbus_parity, and modbus_stop_bits match the inverter configuration, and bench-test with a Modbus simulator (see "Modbus first-light" in §9).
Alerts fire constantly, even on a healthy array.string_stc_w or perf_thresh_pct is set too high.Check that string_stc_w matches your actual string rating. The default 6000 W is for a 15×400 W example, so adjust it. To test the alert path, simulate with an inflated STC value (§9).
No alerts even when strings are visibly shaded or soiled.perf_thresh_pct is too low, or irradiance is below irradiance_min_wm2.Check whether irradiance_wm2 in the summary is below your threshold (100 W/m² by default). In low light, PR evaluation is suppressed on purpose to avoid noise-driven false positives. Raise perf_thresh_pct in Notehub to increase sensitivity (e.g., from 80 to 85), and allow up to 120 minutes for the environment variable to sync.
Idle current is several mA or higher (should be tens of µA).USB/VUSB is connected to the Notecarrier, the ATTN→D6 jumper is missing, or the firmware was built with USB CDC enabled.Disconnect USB from the Notecarrier and power through +VBAT only. Confirm the DIP switch is set to HST (not NC). Check the jumper between ATTN and D6 (§5), and make sure the build used USB support: None (usb=none, §7). With the USB CDC stack enabled and no USB host attached, the host leaves STOP2 within a millisecond.
temp_probe_fault alerts fire.DS18B20 disconnected, out of range (< −40 °C or > 110 °C), or probe is shorted.Check that the DS18B20 data wire goes to D5 with a 4.7 kΩ pull-up to 3V3. Look at the probe tip for corrosion or water damage, and confirm it's attached to a representative panel backsheet.
Pyranometer reads seem consistently low or high.Pyranometer is shaded, or unit calibration differs from nominal.Make sure the sensor is in full sun, co-planar with the array, and away from anything that could shade it. Every sensor has a calibration tolerance, and the default pyranometer_mv_per_wm2_x1000 = 200 (0.200 mV/W/m²) is the Apogee SP-110-SS nominal value. If you have your unit's calibration certificate, update it with that value.

11. Limitations and Next Steps

(The rest of the page points here for the details on each constraint. For commissioning steps, see §9 Validation and Testing above.)

This reference design is for the moment a portfolio operator wants real per-string visibility on an array, with soiling, shading, and bad-module signals showing up on an off-site dashboard within a single sample interval. A handful of details were kept simple so you can get from BOM to first event in one afternoon. Each one is listed below with the production hardening it needs.

Simplified for this reference design

Some of these items simplify the signal model, and the rest simplify the hardware.

Per-string voltage is required for full root-cause classification. The firmware reads a [V, I] register pair for each string and uses the per-string voltage signature to classify root causes. That signal model comes from multi-MPPT string inverters where each MPPT input tracks one string, such as those that implement the SunSpec Model 160 Multiple MPPT register model. That model exposes per-MPPT voltage (DCV) and current (DCA) over Modbus RTU. Traditional string combiner boxes put multiple strings on a shared DC bus and usually expose only per-string current at the combiner's Modbus port, so independent per-string voltages are not available in that topology. Against a combiner-only source, the shading root-cause hypothesis will never fire. It classifies by operating voltage well below the fleet mean, and every string reads the same shared bus voltage, so all the per-string voltage ratios equal 1.0. string_fault also needs a low voltage ratio, so it can't fire either. In that configuration every PR-threshold trip resolves as soiling or degraded, depending on the current magnitude. If the monitored device only provides per-string current and a single bus voltage, document this constraint for O&M personnel, so they understand that a soiling or degraded alert can also mean undetected shading or a string fault.

Register-map demo only. The firmware reads N contiguous 16-bit holding-register pairs [V, I, V, I, …] with fixed scaling from reg_base. Real string combiners and inverters from SMA, Fronius, Huawei, SolarEdge, and ABB each publish their own Modbus register maps. Those maps come with vendor-specific addressing conventions, scaling factors, signed/unsigned handling, and sometimes 32-bit register pairs for accumulated energy, so production deployments need a vendor-specific firmware build that implements the correct map. The environment-variable scaling factors (string_v_scale_x100, string_a_scale_x1000, reg_base) let one firmware binary cover multiple sites with the same vendor, but they are not a substitute for correct implementation of the register map.

All monitored strings must be electrically comparable. A single device should only monitor peer strings on the same array geometry. That means equal module count, equal STC rating, and the same orientation, tilt, azimuth, and irradiance/temperature reference (same MPPT group and microclimate). The PR model uses string_stc_w as one reference power for every monitored string, and the V/I root-cause hypothesis (shading/soiling/string_fault) uses the fleet mean of all monitored strings as its reference. If one device monitors strings with different orientations, different MPPT groups, or unequal lengths, normal production differences between groups will be misclassified as faults. For mixed-geometry arrays, deploy one device per homogeneous string group.

Maximum 4 strings. MAX_STRINGS = 4 is a compile-time constant, but utility-scale installations can have 20–30 strings per combiner. Raising MAX_STRINGS grows sizeof(AppState), the state struct held in RAM across sleep cycles, and expands the note.template payload definition sent on first boot. The real limit is the STM32L433's 64 KB SRAM. AppState, the per-Notefile template JSON construction buffers, and the Modbus response buffer all have to fit together in static and stack memory. The implementation uses no dynamic allocation, so the risk is a growing static footprint that leaves too little headroom for library stack frames, rather than heap fragmentation. Scaling MAX_STRINGS beyond 4 means revalidating that the combined static footprint fits in the 64 KB SRAM budget. Nobody has measured a ceiling for this implementation yet, so validate at each increment before field deployment.

RS-485 interface is POC-grade only. The SparkFun BOB-10124 (SP3485) is a non-isolated breakout board with no surge hardening, fine for bench and prototype use. Solar PV combiner and inverter RS-485 ports are exposed to the outdoor environment and share a ground reference with high-voltage DC bus equipment, so lightning-induced transients and ground-fault currents are a realistic field hazard. Production hardware should replace the BOB-10124 with an isolated RS-485 transceiver (e.g. Analog Devices ADM2587E or similar) that provides galvanic isolation between the host UART and the RS-485 bus. It should also add surge protection on the A/B lines (TVS diodes rated for the bus, IEC 61000-4-5 Class 4 or better). The cable shield must stay single-ended bonded at the combiner/inverter chassis, and outdoor PV installations need proper bonding and grounding per IEC 62548 or NEC Article 690.

Analog pyranometer resolution is good enough for coarse threshold comparisons but limits PR accuracy. Wired straight to A0, the Apogee SP-110-SS gives about 4 W/m² per ADC count at the host's 12-bit / 3.3V reference. A single-supply rail-to-rail op-amp in a 10× non-inverting configuration between the sensor and A0 brings that to about 0.4 W/m² per count, and it's recommended for production deployments. Another option is a pyranometer with Modbus RTU output, such as the IMT Si-RS485TC-T-MB, which also has a Pt1000 module temperature channel. It can replace both the SP-110-SS and the DS18B20 with a single RS-485 device on the same bus.

Single-axis temperature coefficient. The firmware uses one temp_coeff value for the whole array, but bifacial, thin-film, and heterojunction (HJT) modules all have noticeably different temperature coefficients from standard mono-PERC. The default (−0.0035 /°C) is typical for mono-PERC, so set temp_coeff_per10000 to the value on your actual module datasheet. A per-string coefficient would need a more complex state structure.

No irradiance-sensor validation. The firmware uses the pyranometer reading as-is, with no cross-check. A bird dropping on the lens or a shadow from a nearby object makes the firmware compute an artificially low expected power, which makes the PR look high and ends up suppressing real underperformance alerts. A second reference pyranometer or a model-based irradiance estimate (from a local weather API through a Notehub route) would catch this.

Heuristic root-cause hypothesis. The shading/soiling/string-fault classification uses simple thresholds on the ratio of each string's V and I to the fleet mean. It will misclassify strings in some multi-fault scenarios (one string with both shading and partial soiling, for example), and the fleet-mean reference breaks down when most of the strings are underperforming at the same time. Treat the hypothesis as a first-pass maintenance triage signal, not a definitive diagnosis.

Single-string deployments support PR alerts only. When n_strings = 1, the comparative root-cause hypotheses (shading, soiling, string_fault) aren't available. With no peer strings to compare against, every PR-threshold trip comes out as degraded. The device still detects when the string's PR drops below perf_thresh_pct and fires an immediate alert, but the maintenance team has to work out the root cause from a site inspection or other data. If a single-string installation needs root-cause classification, a Modbus-output pyranometer with a reference string (even one extra string of the same geometry) provides the peer comparison.

Mojo is bench-validation equipment only. During commissioning, the Mojo is spliced into the 5V supply rail and connected to the Notecarrier CX with a Qwiic cable so you can check the sleep/wake current profile on the bench. The Notecard reads Mojo's coulomb counter / power monitor over that Qwiic connection and exposes the data through card.power, but the application firmware doesn't call card.power on each sample cycle. The Mojo and its Qwiic cable are not deployed to the field, so remove both before you close up the assembly. If fleet-level energy-consumption telemetry would be useful, adding a runtime mAh field to the summary Note is a straightforward extension: call card.power in firmware and include the result in solar_summary.qo.

Production Next Steps

Once the basic dashboard is live, these extensions move it toward a portfolio-grade monitoring product.

Vendor-specific Modbus register-map implementations for the major inverter/combiner families (SMA SunnyBoy, Fronius Symo, SolarEdge SetApp, Huawei SUN2000) would use SunSpec Models 101 and 103 (single-phase and three-phase inverters) and Model 160 (Multiple MPPT) as the standard reference.

Increase MAX_STRINGS to at least 12 and confirm that total static memory (AppState + template JSON buffers + library stack frames) still fits in the STM32L433's 64 KB SRAM budget.

A Modbus-output pyranometer+temperature combo sensor (e.g. IMT Si-RS485TC-T-MB) folds three separate interfaces into one RS-485 bus.

A shadow-mask calendar would suppress shading alerts during known shadow times, which you can calculate from GPS coordinates and sun-position math, so seasonal early-morning or late-afternoon shadows don't flood the alert channel.

Field-upgradeable firmware through Notecard Outboard Firmware Update lets you push register-map recipe updates to the fleet without a site visit.

Per-device commissioning would record a 7-day baseline PR for each string right after installation, so the alert threshold adapts to site-specific shading and soiling patterns instead of relying on a generic default.

12. Summary

The asset manager used to find out about a soiled or shaded string only when the quarterly energy report showed a yield gap. Now, every five minutes, the device reads what the combiner already knows, puts it in context with the current irradiance and module temperature, and decides whether each string is performing within bounds. It sends a compact summary every hour, plus an alert with a root-cause hypothesis (shading, soiling, or string fault) within about one sample interval (5 minutes by default) of any string falling below the threshold.

The whole footprint is a Notecarrier CX with a Cell+WiFi Notecard, an RS-485 transceiver, an analog pyranometer, and a DS18B20 backsheet probe. None of it touches the existing inverter or asks anything of site IT. Because the cellular-first deployment uses one SKU and one SIM with no per-site configuration, it stays economical across a portfolio of dozens or hundreds of arrays. Instead of a quarterly look-back, the operator gets continuous visibility into the 3–8% of annual production that used to leak away unnoticed.

On This Page

  • 1. Project Overview
  • Before You Start — Critical Constraints
  • 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
    • Sync strategy
    • Retry and error handling
    • Key code snippet 1: Performance Ratio and root-cause hypothesis
    • Key code snippet 2: temperature-derated expected power model
    • Key code snippet 3: immediate-sync alert
    • Key code snippet 4: sleeping until the Notecard raises ATTN
  • 8. Data Flow
  • 9. Validation and Testing
  • 10. Troubleshooting
  • 11. Limitations and Next Steps
    • Simplified for this 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