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_rightEV Charger Session & Utilization Monitor

EV Charger Session & Utilization Monitor

EV Charger Session & Utilization Monitor banner
note

This reference application is here to give you ideas and help you get started quickly. It's built on specific hardware that may not match yours, so focus on the sections that fit your use case. If you'd like to talk through your project and whether it's a good fit for Blues, reach out.

Building with an AI assistant? Point it at this accelerator's firmware in firmware/ev_charger_session_monitor/ so it can learn how the device works and help you adapt it to your hardware.

This project is a cellular energy monitoring retrofit for Level 2 EV chargers. It's for fleet managers, facilities teams, and charging-network operators who need to know how their installed chargers are actually being used. The design wires a DIN-rail energy meter in series with each charger's AC feed and reports three streams over cellular to the Blues Notehub cloud service: per-session metered kWh and peak power, hourly utilization and availability, and mains-offline alerts. A hardware-metered instrument measures the energy. Charger availability comes from the meter's V_rms register, because when mains voltage is absent, the circuit is definitely offline. You don't modify the charger hardware, enroll in OCPP, or involve the site's IT team. The hardware is a Notecarrier CX with a Notecard Cell+WiFi and an EASTRON SDM120-Modbus energy meter (§4 has the BOM), and §10 covers design boundaries and production expansion paths.

1. Project Overview

The problem. Fleet managers, facilities teams, and third-party charging network operators are installing Level 2 EVSE (electric vehicle supply equipment) at workplace parking lots, fleet depots, and retail locations, and almost none of them know how those chargers actually get used. Nobody can tell you whether the 48-amp circuit in Bay 3 runs at 90% utilization while Bay 4 sits idle all day, or whether sessions bunch up in the morning rush so that employees arriving at 9 a.m. find every port taken. Without instrumentation, questions like these get answered by anecdote or not at all.

This project fills that gap with a series-wired DIN-rail energy meter (EASTRON SDM120-Modbus) on the EVSE circuit feed. The device polls it over Modbus RTU and reports session-level energy delivery and port utilization directly, no matter who built the charger, what network it's on, or whether the site team has any standing with the charging network operator. OCPP-based integrations give you richer per-driver and per-RFID records where a charger exposes that interface, but they depend on cooperation from the network operator and vary across Blink, ChargePoint, EV Connect, and in-house fleet stacks. The hardware-meter approach needs none of that. It reads the physics of the circuit, which works on any Level 2 EVSE ever installed.

What this design delivers. You get a metered session and utilization monitor built around a DIN-rail single-phase energy meter (EASTRON SDM120-Modbus or compatible) on the EVSE feed. The device polls the meter every 30 seconds over Modbus RTU. Per-session energy is the difference in the meter's cumulative import-kWh register between session open and close, so the hardware computes it instead of the firmware estimating it from current alone. Peak power (W) is the highest active-power reading seen during the session.

Charger availability comes from the meter's V_rms register. In each reporting window, the firmware counts the minutes the supply voltage stayed above the configurable voltage_present_v threshold (available_min) and divides that by the window's full wall-clock duration (elapsed_min) to get availability_pct. Wakes where the meter can't be reached count against availability in the denominator, whether mains is absent, the wiring is faulted, or every Modbus retry fails. That way the metric correctly drops below 100 % whenever the charger was offline for any reason. A companion sample_coverage_pct field reports the fraction of window time that had valid meter readings, which gives you a data-quality signal separate from availability. A separate mains_absent alert fires when no mains-present reading has shown up for longer than the configurable alert_offline_min threshold. That's a definitive sign of a tripped breaker, a power loss, or a sustained meter fault. Sites that also need per-driver or per-RFID records should add an OCPP back-end integration alongside this meter-based monitor. See §10 for boundaries and expansion paths.

Why Notecard. The network is the biggest headache in instrumenting EV chargers. Chargers in a corporate parking structure almost always sit on a circuit run by a third-party operator (a fleet management company, a charging network, or a facilities contractor) whose people have zero standing with the site's IT department. IT treats the parking lot as a hostile network zone. Nobody hands out WiFi credentials or provisions a VLAN, and no IT ticket is getting approved by Friday. Even for in-house fleet teams, "just connect it to the corporate network" means months of security review for every new device class.

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.

Cellular avoids all of that. A Notecard Cell+WiFi (MBGLW) with its included prepaid SIM goes in the same electrical panel as the charger circuit, connects to an LTE Cat-1 bis tower from the parking lot, and starts reporting session data within minutes. Site IT never gets involved, and there's no network form to fill out or AP to pair with. WiFi stays available as an opportunistic fallback for the rare site with a legitimate parking-structure AP, but you never need it. The same firmware image and hardware SKU deploy the same way at a workplace, a fleet depot, or a multi-tenant retail center.

Deployment scenario. A DIN-rail assembly mounts inside the electrical panel that feeds the EVSE circuit. The SDM120-Modbus energy meter is wired in series on the EVSE circuit (the live conductor runs through the meter's current input), and a SparkFun BOB-10124 (SP3485) RS-485 transceiver breakout bridges the meter's RS-485 port to the host's 3.3 V UART. The Notecarrier CX and Notecard MBGLW run from a 5 V / 2.4 A DIN-rail supply (e.g. MeanWell HDR-15-5) fed from whatever mains voltage the panel has. That's usually 120 VAC line-to-neutral where a neutral is accessible at the subpanel, or 208/240 VAC line-to-line where only two line conductors are available. The HDR-15-5 accepts 85–264 VAC, so it handles both. The whole assembly takes four to six DIN-rail modules of panel space, and you don't modify the charger hardware, enroll in OCPP, or coordinate with IT.

Panel placement note. Whether you can mount the DIN-rail assembly inside the main EVSE panel depends on the enclosure's listing and the local electrical code, since not every listed enclosure allows third-party low-voltage auxiliary equipment in the mains compartment. If the panel interior isn't an option, mount the assembly in a separately listed auxiliary enclosure bolted next to the main panel, and run the RS-485 signal cable and AC supply conductors between the two enclosures through a listed conduit entry or knockout. When you can't confirm that the main panel is suitable, the auxiliary enclosure is the safe default.

2. System Architecture

System architecture: SDM120 energy meter via Modbus RTU → Notecarrier CX with STM32 host and Notecard MBGLW → cellular → Notehub → utilization / analytics / on-call

Device-side responsibilities. Everything in the panel runs on a 30-second loop. The Notecarrier CX's onboard STM32L433 host wakes when the Notecard raises its ATTN pin at the end of a card.attn sleep. It polls the SDM120 over Modbus RTU for V_rms, active power, and import kWh, then runs a two-state session machine that decides whether the charger is actively delivering energy. Per-session energy is the difference in the meter's cumulative import-kWh register between session open and close, so the firmware never has to estimate from current alone or assume a power factor. Availability gets tracked on every wake as well. window_elapsed_sec advances unconditionally (wall-clock time), while window_available_sec only advances when the meter is valid and V_rms ≥ voltage_present_v. The resulting availability_pct counts any wake where the circuit was offline or unreadable against the charger, instead of hiding it. When a session closes, the host queues a charger_session.qo Note right away with sync:true, and window statistics queue into charger_summary.qo on the report cadence. Between wakes the host sleeps in STM32 STOP2 (a few µA) with its RAM intact, so session and window state just stays in memory. The host is usually awake for less than two seconds per wake.

Notecard responsibilities. The Notecard handles the network. It queues Notes locally, opens the cellular (or WiFi) session on the configured hub.set outbound cadence (60 minutes by default), and flushes any sync:true session or alert Note the moment it arrives. It also distributes environment variables from Notehub, so an operator can retune thresholds and reporting intervals across a fleet of panels without re-flashing firmware.

Notehub responsibilities. The Notecard's embedded global SIM carries Notes from the panel over supported carriers worldwide and delivers them to Notehub. Three Notefiles arrive separately so each can take its own path downstream. charger_session.qo (per completed session, real-time) goes to a utilization dashboard or fleet management system, charger_summary.qo (periodic) goes to a long-term analytics store, and charger_alert.qo (mains-absent / charger-offline alert) goes to an on-call endpoint. Smart Fleets are the natural way to organize devices. With one fleet per property, environment-variable overrides (a site's known line voltage, say) stay scoped to the right chargers.

Routing to the cloud (high level). Notehub can route to HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations. Route setup depends on your project and is outside the scope of this reference, so see the Notehub routing docs for configuration details.

3. Technical Summary

If you want to see data before reading the rest, here's the fastest path from parts to a first event in Notehub:

  1. Create a Notehub project at notehub.io and copy its ProductUID (it looks like com.your-company:ev-charger-monitor).
  2. Wire the bench rig: Notecarrier CX + Notecard MBGLW + SDM120-Modbus energy meter + SparkFun BOB-10124 RS-485 transceiver (D0/D1/D2 pins), plus the ATTN→D5 jumper for host sleep. §5 has the full wiring details.
  3. Set your ProductUID: the sketch deliberately has no PRODUCT_UID placeholder to edit, and it refuses to compile (#error) until one is defined. Either add #define PRODUCT_UID "com.your-company:ev-charger-monitor" near the top of firmware/ev_charger_session_monitor/ev_charger_session_monitor.ino (above the #ifndef PRODUCT_UID guard), or pass it as a build flag with arduino-cli (see §7.1).
  4. Flash: compile and upload with the board options STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none (§7.1 has the arduino-cli commands and IDE build settings). The usb=none option turns off the USB CDC stack, which has to be off for the host to sleep.
  5. Watch Notehub: open your project's Events tab. You'll see _session.qo within seconds, which proves the radio works, then a charger_summary.qo within the hour and a charger_session.qo after you run a test load (see §9).

The rest of this page covers component selection, wiring, Notehub configuration, and validation in detail.

Here is a sample Note this device emits:

{
  "sessions": 1,
  "total_kwh": 5.9,
  "avg_session_kwh": 5.9,
  "peak_w": 10320.0,
  "charging_min": 38,
  "idle_min": 22,
  "utilization_pct": 63.3,
  "available_min": 60,
  "availability_pct": 100.0,
  "sample_coverage_pct": 100.0
}

4. Hardware Requirements

PartQtyRationale
Notecarrier CX1Integrated carrier with an onboard STM32L433 host, so you don't need a separate MCU. To keep idle draw as low as possible, the host sleeps in STOP2 between samples and the Notecard's ATTN pin wakes it.
Notecard Cell+WiFi (MBGLW)1LTE Cat-1 bis with global coverage and a WiFi fallback. Cellular is the primary path, and §1 explains why site WiFi is rarely an option in EV charger deployments. The included SIM comes with 500 MB of data and 10 years of service. For technical details, see the MBGLW datasheet.
Blues Mojo (bench validation only)1Coulomb counter for measuring per-wake energy and checking the sleep/transmit power profile during bring-up. It isn't deployed to the field. See §9.
SparkFun CEL-16432 LTE Hinged External Antenna, 698–2700 MHz, SMA Male1Required for reliable signal from inside a metal enclosure. The SMA connector screws onto the SMA female pigtail at the panel wall, which puts the radiating element outside the metal cabinet. A rubber-duck antenna left inside a steel panel will lose a lot of signal.
SparkFun WRL-18568 SMA to U.FL Cable, 150 mm1Connects the Notecard's U.FL cellular port to the SMA female bulkhead connector in the panel wall. If the Notecarrier mounts farther from the wall, a longer SMA-to-U.FL pigtail from Mouser or DigiKey can substitute.
EASTRON SDM120-Modbus 45 A direct-connect single-phase DIN-rail energy meter1Wired in series on the EVSE circuit (the live conductor runs through the current input terminals). Measures V_rms, active power (W), and cumulative import kWh with Class 1 accuracy, and its RS-485 Modbus RTU interface connects to the host UART. It's rated 45 A direct-connect, which covers 32 A and 40 A Level 2 EVSE. For 48 A or 60 A EVSE circuits, use the SDM120CT variant with a suitable 5 A secondary split-core CT, or the SDM230-Modbus (100 A direct). The SDM120 needs both a live conductor and a neutral terminal for voltage sensing, so confirm both are accessible at the panel before you specify this meter. The Modbus protocol document (register map, baud-rate defaults, function codes) is a PDF download after free registration on the Eastron Europe website.
SparkFun RS-485 Transceiver Breakout – SP3485 (BOB-10124)1Converts the host's 3.3 V UART to the RS-485 differential pair the SDM120 needs. The SP3485 chip runs from a single 3.3 V supply, so you avoid the logic-level compatibility problems that come with 5 V-only transceivers. Wire RO to D0 (Serial1 RX), DI to D1 (TX), and the RTS pad to D2. The board routes the RTS pad to both DE (driver enable) and RE (receiver enable) internally, so you don't need a separate jumper. Then wire VCC to +3V3 and GND to GND.
AC/DC DIN-rail supply, 85–264VAC input, 5 V / 2.4 A output (e.g. MeanWell HDR-15-5)1Makes 5 V DC from whatever mains voltage the panel has (120 VAC L-N or 208/240 VAC L-L). The 2.4 A output rating leaves enough transient headroom for Notecard cellular transmit bursts (≤2 A peak), and the DIN-rail mount fits cleanly in the panel enclosure next to the EVSE breaker.
DIN-rail enclosure, ≥6-module width1Houses the Notecarrier CX, DIN-rail supply, and RS-485 transceiver module inside the electrical panel. The SDM120 itself is a separate DIN-rail device wired in series on the EVSE circuit.

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

Wiring: SDM120 energy meter via BOB-10124 RS-485 transceiver; ATTN → D5 jumper so the Notecard can wake the host from STOP2; cellular antenna via u.FL; bench Mojo on Qwiic; 120/240 VAC → HDR-15-5 → Mojo → +VBAT

warning

Electrical safety. The 120/240VAC feed inside an electrical panel is hazardous. This installation puts the SDM120 energy meter in series on the EVSE feed, which means interrupting and re-terminating the live mains conductors. Panel work must be performed by a qualified electrician following the applicable electrical codes and lockout/tagout procedures.

warning

Panel placement. Whether you can mount third-party low-voltage electronics inside a mains EVSE panel depends on the enclosure's UL/CSA listing and the local electrical code. If the panel listing or jurisdiction doesn't allow auxiliary equipment in the mains compartment, install the Notecarrier CX, power supply, and RS-485 transceiver module in a separately listed auxiliary enclosure mounted next to the main panel. Run the RS-485 signal cable and AC supply conductors between the two enclosures through a listed conduit entry or knockout. Any time you can't confirm the main panel's interior is suitable, go with the auxiliary enclosure. It's the safe default.

SDM120 meter installation. The SDM120-Modbus is a series element, so the EVSE circuit's live conductor has to pass through the meter's current input terminal. Break the live conductor feeding the EVSE and route it through the meter's current input terminals, with line-in on the input terminal and line-out on the output terminal. Follow the terminal labels printed on the SDM120 faceplate exactly. Designations vary by variant and firmware revision, so don't rely on a generic reference for this wiring. Connect the neutral conductor to the meter's neutral terminal, because the SDM120 needs a neutral connection for its internal voltage measurement. Do not route the neutral through the current input. The SDM120 powers itself from the same line and neutral conductors that feed the EVSE, and the datasheet shows the exact supply terminal connections for your variant. Wiring the meter interrupts the EVSE circuit, so follow lockout/tagout procedures and have a qualified electrician do the work.

note

Circuit ampacity. The SDM120-Modbus is rated 45 A continuous, which suits 32 A EVSE circuits (40 A breaker) and 40 A EVSE circuits (50 A breaker). For 48 A EVSE (60 A breaker) or higher, substitute the SDM120CT variant with a suitably rated split-core CT, or the SDM230-Modbus (100 A rated). §10 has more substitution guidance.

RS-485 wiring. Use twisted-pair cable (120 Ω characteristic impedance recommended) to connect the SDM120's RS-485 terminals (A+ and B−) to the matching A and B terminals on the SparkFun BOB-10124 breakout, through either the 3.5 mm screw terminal or the 0.1" header. Then wire the breakout to the Notecarrier CX header:

  • BOB-10124 RO (receiver output) → D0 (Serial1 RX) on the Notecarrier CX dual 16-pin header
  • BOB-10124 DI (driver input) → D1 (Serial1 TX)
  • BOB-10124 RTS (direction control) → D2 (the board routes this pad to both DE and RE internally)
  • BOB-10124 VCC → +3V3
  • BOB-10124 GND → GND

The firmware drives D2 HIGH before it transmits a Modbus request frame and LOW right after, which switches the transceiver from receive to transmit and back. A 120 Ω termination resistor across the A+ and B− terminals at the far end of the RS-485 bus cuts down on reflections, but at the short cable lengths you'll usually have in a panel enclosure (< 1 m), it's optional.

Notecard and antenna. Seat the Notecard Cell+WiFi (MBGLW) in the Notecarrier CX's M.2 slot. Connect the u.FL-to-SMA pigtail to the Notecard's cellular u.FL port, thread the SMA end through a cable gland in the panel enclosure wall, and attach the external antenna outside the panel. A rubber-duck antenna left inside a metal panel will lose a lot of signal. The Notecard talks to the STM32 host over I²C on the internal Notecarrier bus, so that path needs no extra wiring.

ATTN-to-D5 jumper (host sleep). Fit a short jumper between the ATTN and D5 pins on the Notecarrier CX. Both sit on the same 16-pin header (ATTN at position 3, D5 at position 14). The Notecard pulls ATTN low while the host sleeps and raises it when the next poll is due, and the rising edge on D5 wakes the host from STOP2. Leave the Notecarrier CX EN pin unconnected. On the CX it enables the shared 3.3 V VIO rail (host, Qwiic, 3V3_OUT, and Notecard I/O), so driving it from ATTN would brown out the whole board instead of putting the host to sleep. See Putting a Host to Sleep Between Sensor Readings for the full pattern.

Power. Wire mains power to the MeanWell HDR-15-5 input terminals. Use 120 VAC line-to-neutral where a neutral conductor is accessible at the panel, or 208/240 VAC line-to-line where only two line conductors are available. The HDR-15-5 accepts 85–264 VAC and handles either configuration, but check the local electrical code for the permitted supply tap. Connect the HDR-15-5's 5V and GND output terminals to the Notecarrier CX's +VBAT and GND pads. For bench bring-up, put the Mojo in series between the HDR-15-5's 5V output and the +VBAT pad, and connect the Mojo's Qwiic port to the Notecarrier CX's Qwiic connector. §9 describes what to look for on the trace.

Pin summary:

  • D0 (Serial1 RX) → BOB-10124 RO
  • D1 (Serial1 TX) → BOB-10124 DI
  • D2 → BOB-10124 RTS (routes to DE + RE internally)
  • +3V3 → BOB-10124 VCC
  • GND → BOB-10124 GND
  • ATTN → D5 (jumper; Notecard wakes the host from STOP2)
  • SDA / SCL → Notecard (internal to the Notecarrier; onboard pull-ups present)
  • +VBAT → 5V from HDR-15-5 (via Mojo on bench)
  • Cellular u.FL → pigtail to external SMA antenna outside the panel

6. Notehub Setup

  1. Create a project. Sign up at notehub.io and create a project. Copy the ProductUID, which looks like com.your-company.your-name:ev-charger-monitor, and define it as PRODUCT_UID. Either add #define PRODUCT_UID "com.your-company:ev-charger-monitor" above the #ifndef PRODUCT_UID guard in firmware/ev_charger_session_monitor/ev_charger_session_monitor.ino, or pass it as a build flag: arduino-cli compile ... --build-property 'compiler.cpp.extra_flags=-DPRODUCT_UID="com.your-company:ev-charger-monitor"'. The sketch stops with a compile-time #error if PRODUCT_UID is undefined, so nobody can flash a misconfigured binary by accident. For bench work before you have a Notehub project, -DALLOW_EMPTY_PRODUCT_UID suppresses the error. Never ship a build with that flag.

  2. 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 Devices within a minute or two.

  3. Create a Fleet per property. Fleets group devices for shared configuration and routing. One fleet per property or tenant is the natural unit here, since every charger at a given site shares the same line voltage, session-detection thresholds, and routing destinations. Smart Fleets can auto-assign new devices to the right fleet at commissioning time. All devices in a single Notehub project share the same ProductUID, though, so fleet rules need to key on a device attribute that varies by site. That could be an installer-applied tag (e.g., site:depot-north), a serial/UID range reserved for each property during provisioning, or location metadata filled in during the first sync.

  4. Set environment variables. In Notehub, open the Fleet → Environment tab (or Device → Environment for a per-device override). Add or update any of the variables below, and the device fetches them on its next inbound sync without a reflash. Every variable is optional. If you don't set one, the firmware default shown below applies.

    VariableDefaultPurpose
    sample_interval_sec30Seconds between host wakes and Modbus meter polls.
    report_interval_min60Minutes between charger_summary.qo Notes. Changing this also re-applies hub.set so the Notecard's outbound cadence stays in sync.
    session_threshold_w500Active power (W) above which a charging session counts as open. Level 2 EVSE minimum pilot current (6 A × 208 V) is ~1250 W, so 500 W sits safely below any real session but above charger standby draw.
    session_end_count3Consecutive below-threshold wakes before a session is closed. At the default 30-second interval, that's a 90-second grace period that rides out brief power dips during EV charge-phase transitions.
    voltage_present_v85V_rms floor for mains-present classification. Readings below this threshold count against charger availability and start the alert_offline_min timer. 85 V is below the sag floor for both 120 V and 208/240 V feeds.
    alert_offline_min240Minutes without a mains-present reading before a charger_alert.qo Note fires. Set to 0 to disable offline alerting entirely.
    modbus_slave_id1Modbus slave address of the SDM120. The SDM120 factory default is 1. Change it if you have multiple meters on the same bus.
    modbus_baud9600Modbus RTU baud rate. It must match the SDM120's configured baud. The factory default is 2400 on older units and 9600 on most current stock, so check your meter's display or documentation.
  5. Configure routes. At minimum, add one route for charger_session.qo (real-time delivery to a fleet management system or dashboard webhook) and a second for charger_summary.qo (long-term analytics or historian). If the mains-absent alert matters at your sites, add a third route for charger_alert.qo to an on-call or ticketing endpoint. The three Notefiles are kept separate so each one can go out at its own urgency, with no filter logic in the route.

What you should see in Notehub

Three event types matter:

  • _session.qo: automatic Notecard housekeeping on each cellular session. It confirms the radio is reaching Notehub.
  • charger_summary.qo: one per report_interval_min (hourly by default). Here's a sample body for a 60-minute window with one session:
    {
      "sessions": 1,
      "total_kwh": 5.9,
      "avg_session_kwh": 5.9,
      "peak_w": 10320.0,
      "charging_min": 38,
      "idle_min": 22,
      "utilization_pct": 63.3,
      "available_min": 60,
      "availability_pct": 100.0,
      "sample_coverage_pct": 100.0
    }
    total_kwh is the SDM120's import-kWh register delta: the most recent valid closing reading minus the register value when the window opened. That's actual metered energy, not an estimate. If the meter poll fails on the exact wake when the summary fires, the firmware uses the most recent valid reading as the closing value, so total_kwh stays accurate and the next window's kWh baseline isn't corrupted. By construction, charging_min + idle_min always equals total_min. total_min counts only wakes where the meter returned valid data. Wakes where every Modbus retry failed stay out of the utilization accumulators, so transient bus faults can't inflate idle time or distort utilization_pct. The formula is utilization_pct = charging_min / total_min × 100. available_min is the number of minutes V_rms was ≥ voltage_present_v, and availability_pct = available_min / elapsed_min × 100. Here elapsed_min is the full wall-clock window duration, including wakes where the meter was unreachable, which count as unavailable. sample_coverage_pct = total_min / elapsed_min × 100 shows the fraction of wall-clock window time that had valid meter readings. A value of 100.0 means every poll succeeded, and anything lower flags Modbus or mains downtime during the window. Take a fully energized window where every poll succeeded and the charger was in use for 38 of 60 minutes: available_min = 60, availability_pct = 100.0, sample_coverage_pct = 100.0, and utilization_pct = 63.3.
  • charger_session.qo: one per completed charging session, sent immediately (sync:true). Sample body:
    {
      "session_kwh": 10.2,
      "duration_min": 63.5,
      "peak_w": 10320.0,
      "start_epoch": 1746283200,
      "timing_valid": true
    }
    session_kwh is the SDM120's import-kWh delta (close reading minus open reading), so it's the actual metered energy for this session. peak_w is the highest active-power reading seen during the session. Both fields are always valid, whatever timing_valid says. If a session opened before the Notecard finished its first cellular time sync, timing_valid is false and duration_min and start_epoch are emitted as 0. Suppress or flag records with timing_valid: false in downstream analytics, though you can still trust their session_kwh and peak_w.
  • charger_alert.qo: sent once, then suppressed until mains returns, when alert_offline_min minutes pass without a mains-present V_rms reading. The timer counts from the last confirmed mains-present epoch. If mains has never been confirmed, it counts from the device's first valid epoch after commissioning, so the alert fires even for a circuit that's been dead since day one. Sample body:
    {
      "alert": "mains_absent",
      "offline_min": 248
    }

7. Firmware Design

The firmware lives in four files inside firmware/ev_charger_session_monitor/:

  • ev_charger_session_monitor.ino has setup(), loop(), and the top-level app flow (one-time init, per-wake Modbus meter poll, sleep scheduling).
  • ev_charger_session_monitor_helpers.h holds shared constants, the State struct, extern declarations, and function prototypes.
  • ev_charger_session_monitor_helpers.cpp has all the helper implementations (Modbus polling, session state machine, Note emission, env-var handling, host sleep).
  • cx_sleep.h handles host sleep for the Notecarrier CX: the card.attn request, STOP2 entry, and ATTN-pin wake.

7.1 Installing and flashing

Dependencies:

  • Arduino core for STM32 (stm32duino/Arduino_Core_STM32). Add the board manager URL https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json under File → Preferences → Additional Boards Manager URLs. The Notecarrier CX's host uses the Cygnet board definition (see the build settings below).
  • Blues Wireless Notecard library (note-arduino). Install it from the Arduino Library Manager (arduino-cli lib install "Blues Wireless Notecard") or search for "Blues Wireless Notecard" in the IDE Library Manager. Check the note-arduino releases page for any newer stable version.
  • ModbusMaster library by Doc Walker, available in the Arduino Library Manager. Install it with arduino-cli lib install "ModbusMaster" or search for "ModbusMaster" in the IDE, and use version ≥ 2.0.1. It provides the Modbus RTU master implementation the firmware uses to talk to the SDM120 over Serial1.
  • STM32duino Low Power and its dependency STM32duino RTC (stm32duino/STM32LowPower). Install them with arduino-cli lib install "STM32duino Low Power" "STM32duino RTC" or from the IDE Library Manager. They provide 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 (or the Notecarrier CX USB-C switch is in NC), the STM32's USB wakeup interrupt fires continuously and the host never stays in STOP2. Debug output goes to the LPUART on the Notecarrier CX debug jack instead, which an ST-LINK V3 exposes as a virtual COM port (see Serial Logging With STLINK). Serial1 on D0/D1 stays dedicated to the RS-485 link.

Flashing with the Arduino IDE: Open ev_charger_session_monitor.ino, apply the build settings above, and hit Upload. With the Notecarrier CX's USB-C switch in HST, you can reach the host's DFU bootloader 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: from the firmware directory, run:

# Typical modern stm32duino core
cd firmware/ev_charger_session_monitor
arduino-cli lib install "Blues Wireless Notecard" "ModbusMaster" "STM32duino Low Power" "STM32duino RTC"
# usb=none disables the USB CDC stack, which must be off for the host to sleep.
# PRODUCT_UID must be defined or the build fails with #error (see §6 step 1).
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none \
  --build-property 'compiler.cpp.extra_flags=-DPRODUCT_UID="com.your-company:ev-charger-monitor"' .
arduino-cli upload  -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/cu.usbmodem* .

sketch.yaml in that directory sets the same board options, so arduino-cli compile without -b picks them up too. Replace /dev/cu.usbmodem* with the port the Notecarrier enumerates on your machine, usually COMx on Windows, /dev/ttyACM* on Linux, or /dev/cu.usbmodem* on macOS.

After flashing, open a serial monitor at 115200 baud on the ST-LINK virtual COM port. On power-up you'll see [app] power-up — will configure Notecard. Once the meter is connected, each wake prints [app] V=240.1 V P=0 W kWh=12.345 and then goes quiet for 30 seconds while the host sleeps in STOP2. If the meter is missing, unpowered, or miswired, you'll see [app] WARN: Modbus voltage read error 0xE2 (response timeout) on every wake. A 0x02 error means the slave responded but rejected the register address, which usually points to a slave-ID or register-map mismatch. The library header documents the other ModbusMaster error codes.

7.2 Modules

ResponsibilityFunction
Notecard configuration (hub.set, template registration, accelerometer disable)initNotecard, defineTemplates
RS-485 / Modbus initialization (once at power-up, and on baud/ID change)initModbus
Time-filtered environment variable fetchfetchEnvOverrides
SDM120 Modbus RTU poll (V_rms, active power, import kWh)pollMeter
Epoch time from NotecardgetEpoch
Session state machine (IDLE ↔ CHARGING) with availability trackingrunSessionStateMachine
Per-session Note emissionemitSessionNote
Hourly summary Note emission and window resetemitSummaryNote
Mains-absent alert emissionemitOfflineAlert
Host sleep (STOP2 until the Notecard raises ATTN)sleepHost → cxSleepUntilAttn (cx_sleep.h)

7.3 Sensor reading strategy

pollMeter sends the SDM120 three sequential Modbus RTU read transactions (Function Code 0x04, input registers), one for each parameter it needs: V_rms (register 0x0000), active power in Watts (register 0x000C), and cumulative import energy in kWh (register 0x0048). Each parameter is a big-endian IEEE-754 float stored across two consecutive 16-bit registers. A 20 ms pause between transactions gives the SDM120 enough time to respond at 9600 baud, since a full 9-byte response frame takes under 10 ms. If any read returns a non-zero Modbus error code, pollMeter retries the whole three-register sequence up to three times, with a 50 ms pause between attempts. That absorbs the single-frame bus collisions and turnaround-timing faults that show up most often at power-on or after a brief conductor disturbance.

If all three attempts fail, which points to an unpowered meter, faulty wiring, or a misconfigured slave ID, pollMeter returns valid = false. runSessionStateMachine then suppresses every session-state transition and all window accumulation (charging_sec, idle_sec) for that wake. A session in progress is neither extended nor closed (its below_threshold_count doesn't increment), and the failed sample adds no idle time. window_elapsed_sec still advances unconditionally, though, so the failed poll counts against availability_pct as an unavailable interval and lowers sample_coverage_pct. The mains-absent offline timer keeps advancing on its own (via last_mains_epoch in loop()), so a sustained meter fault still triggers the mains_absent alert after alert_offline_min minutes.

The SDM120 does all the V and I measurement and math internally, and the host just reads the finished result registers. The Notecarrier side needs no ADC sampling, no voltage or power-factor assumptions, and no analog front-end components.

7.4 Event payload design

charger_summary.qo uses a template that encodes each record as ~40 bytes on the wire, compared with ~200 bytes as free-form JSON. At one record per hour per charger across a 50-unit fleet, that adds up over the life of a 500 MB SIM. The template is registered once at cold boot and survives Notecard reboots.

charger_session.qo and charger_alert.qo are left untemplated on purpose. Session Notes are infrequent (one per EV charge, usually 1–8 per day), and the alert body has a different shape from the session body. Leaving both untemplated avoids the schema conflict you'd get from mixing two different body shapes under one template.

Template definition snippet:

bool defineTemplates() {
    J *req  = notecard.newRequest("note.template");
    JAddStringToObject(req, "file", FILE_SUMMARY);
    JAddNumberToObject(req, "port", 50);
    J *body = JAddObjectToObject(req, "body");
    JAddNumberToObject(body, "sessions",             12);    // completed sessions in window
    JAddNumberToObject(body, "total_kwh",            14.1);  // metered energy delivered
    JAddNumberToObject(body, "avg_session_kwh",      14.1);  // per-session average
    JAddNumberToObject(body, "peak_w",               14.1);  // peak active power in window
    JAddNumberToObject(body, "charging_min",         12);    // minutes delivering energy
    JAddNumberToObject(body, "idle_min",             12);    // minutes not delivering energy
    JAddNumberToObject(body, "utilization_pct",      14.1);  // charging_min / total_min × 100
    JAddNumberToObject(body, "available_min",        12);    // minutes with V_rms ≥ floor
    JAddNumberToObject(body, "availability_pct",     14.1);  // available_min / elapsed_min × 100
    JAddNumberToObject(body, "sample_coverage_pct",  14.1);  // total_min / elapsed_min × 100
    bool ok = notecard.sendRequest(req);
    if (!ok) debugSerial.println("[app] WARN: note.template failed — will retry on next wake");
    else     debugSerial.println("[app] Notefile template registered for " FILE_SUMMARY);
    return ok;
}

Template notation explained: 12 means a 2-byte signed integer (range -32,768 to 32,767), and 14.1 means a 4-byte IEEE-754 float (the .1 marks the field as a float and doesn't set its precision). The template shrinks each full summary record from roughly 200 bytes of free-form JSON to about 40 bytes on the wire, which matters over a 500 MB SIM's lifetime at one record per hour per device.

7.5 Low-power strategy

The device is grid-tied, so the absolute energy budget isn't a big concern. A disciplined sleep pattern still matters, though. It keeps the enclosure cooler, reduces component stress, and gives you firmware that ports directly to a battery- or solar-backed variant without a rewrite.

After each 30-second wake, sleepHost() calls cxSleepUntilAttn() (in cx_sleep.h). That function sends a card.attn sleep request for sample_interval_sec seconds, waits for the Notecard to pull ATTN low, and puts the STM32 host 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 D5 wakes the host, and execution picks up on the next line of loop(). Because the host resumes in place instead of cold-booting, the State struct (session baseline, window accumulators, pending Note, cached config) just lives in RAM. Nothing is serialized to the Notecard, and setup() runs only once per power-up. If the sleep request fails or ATTN never goes low (a missing jumper, for example), sleepHost() waits out the interval awake and tries again on the next cycle, so the 30-second cadence holds either way.

Between cellular sessions the Notecard idles at 8–18 µA. The Modbus poll adds roughly 100–200 ms to each wake (three transactions at 9600 baud with 20 ms pauses), but the host MCU is still only active for about one to two seconds out of every 30. The radio runs about once an hour, plus any session-end sync events.

hub.set runs in periodic mode, with outbound matching report_interval_min (60 min by default) and inbound at 120 min. Session-end Notes set sync:true, so the Notecard opens a cellular session right away instead of waiting for the outbound timer. With the default session_end_count=3 and a 30-second sample interval, the state machine waits for 3 consecutive below-threshold wakes (a 90-second grace period) before it formally closes the session and queues the Note. The cellular sync then adds roughly 15–60 seconds. With default settings, expect a completed session to show up in Notehub roughly 2–3 minutes after the car unplugs. Lowering session_end_count (to 1, say) shortens the grace period, but it raises the risk of falsely closing a session during EV charge-phase transitions.

7.6 Retry and error handling

  • pollMeter retries the complete three-register Modbus sequence up to three times per wake before it declares a poll failure. When every retry fails, runSessionStateMachine suppresses all session-state transitions and all window accumulation (charging_sec, idle_sec) for that wake. A session already in progress stays open (its below_threshold_count doesn't increment), and the failed sample adds no idle time. window_elapsed_sec still advances, as it does on every wake, so the failed poll counts against availability_pct in the denominator and also lowers sample_coverage_pct. The mains-absent offline timer keeps advancing regardless, so a sustained meter fault still triggers the mains_absent alert after alert_offline_min minutes.
  • fetchEnvOverrides passes the stored env_last_modified timestamp to env.get's time filter. If nothing has changed since the last fetch, the Notecard answers with an {env-not-modified} error, and the function returns right away without parsing anything or touching the config, which keeps I²C overhead minimal. Environment variable values always arrive as strings, so the firmware parses each one with strtod() before range-checking it. A change to modbus_slave_id or modbus_baud re-runs initModbus() on the spot.
  • getEpoch returns 0 until the Notecard has synced and has the time. The summary Note only fires when now > 0 and window_start_epoch > 0, so it can't go out before a cellular session is established. The mains-absent alert is guarded by offline_ref > 0, where offline_ref is the last mains-present epoch, or window_start_epoch if mains has never been confirmed. That keeps the alert quiet until the device has a valid clock and the configured absence duration has really elapsed.
  • emitSessionNote sets timing_valid = false when the start_epoch parameter is 0, which happens when a session opened before the Notecard finished its first cellular time sync. duration_min and start_epoch would be wildly wrong in that case, so both go out as 0, and timing_valid: false in the Note body tells downstream consumers to suppress them. session_kwh and peak_w aren't affected and are always emitted normally.
  • A change to report_interval_min re-applies hub.set through applyHubCadence(). The hub_cadence_dirty flag is set before the attempt and only cleared on confirmed success, so one transient I²C failure can't leave the Notecard stuck on the old outbound cadence. Like the notecard_configured retry pattern, loop() retries on every later wake until the cadence update is confirmed.
  • If emitSessionNote() fails at session end, the completed payload goes into a dedicated pending-note struct (pending_session_*) and session_active is cleared right away. runSessionStateMachine() retries the pending Note at the top of each later wake, but it always falls through to normal state-machine processing whether or not the retry worked. That way a back-to-back charging session during a transient Notecard fault still gets detected and opened, instead of being silently blocked by a retry loop. Retry-path window attribution: if the hourly summary fires and resets the window accumulators between the session-end failure and a later successful retry, the session's count and completed-session kWh land in the newer window, not the one where the session actually ended. §10 covers this edge case in more detail.
  • alert_offline_min accepts 0 or any positive integer. Set it to 0 to disable mains-absent alerting, or to a positive integer to adjust the threshold. The firmware only applies a variable that's set and non-empty, so a missing env var leaves the current setting alone. That also means deleting a variable in Notehub keeps its last value until the next power cycle, when the firmware defaults come back.

7.7 Key code snippet: session-end Note with immediate sync

When the session state machine closes a session, it computes session_kwh from the meter's import-kWh register delta, stores the payload in a pending-note struct (pending_session_*), and clears session_active immediately before calling emitSessionNote. Every payload value goes in as an explicit parameter, so the function behaves the same at first close and during pending-note retries, after session state has been cleared and maybe overwritten by a new session. A timing_valid guard covers sessions that opened before the Notecard had a valid epoch. If start_epoch is 0, duration_min and start_epoch would be wildly wrong, so they're emitted as 0 and timing_valid is set to false. session_kwh and peak_w are valid either way.

bool emitSessionNote(float kwh, float peak_w,
                     uint32_t start_epoch, uint32_t end_epoch) {
    bool     timing_valid = (start_epoch > 0 && end_epoch >= start_epoch);
    uint32_t dur_sec      = timing_valid ? (end_epoch - start_epoch) : 0;

    J *req  = notecard.newRequest("note.add");
    JAddStringToObject(req, "file", FILE_SESSION);
    JAddBoolToObject(req,   "sync", true);
    J *body = JAddObjectToObject(req, "body");
    JAddNumberToObject(body, "session_kwh",  kwh);
    JAddNumberToObject(body, "duration_min", (float)dur_sec / 60.0f);
    JAddNumberToObject(body, "peak_w",       peak_w);
    JAddNumberToObject(body, "start_epoch",  timing_valid ? (double)start_epoch : 0.0);
    JAddBoolToObject(body,   "timing_valid", timing_valid);
    // ...
}

7.8 Key code snippet: sleeping until the Notecard raises ATTN

cxSleepUntilAttn() asks the Notecard to hold ATTN low for exactly sample_interval_sec seconds, confirms the pin has gone low, and puts the STM32 into STOP2. Since the host stays alive, the card.attn call is a request with a checked result, not a fire-and-forget command. STOP2 retains RAM, so the session-start kWh baseline, peak power, and hourly window state are all still there when loop() runs again. setup() arms the ATTN wake once with cxSleepBegin(), and every loop() pass ends with a call to sleepHost():

void sleepHost() {
    uint32_t sleep_sec = state.sample_interval_sec;
    if (!cxSleepUntilAttn(notecard, sleep_sec, NULL, &debugSerial)) {
        // The Notecard didn't take the sleep request, or ATTN never went low
        // (check the ATTN -> D5 jumper).  Keep the sample cadence and retry.
        debugSerial.println("[app] WARN: ATTN sleep failed — waiting out the interval awake");
        delay(sleep_sec * 1000UL);
    }
}

7.9 Key code snippet: Modbus meter poll

pollMeter reads three SDM120 input-register pairs in sequence. Each pair is a big-endian IEEE-754 float, and regsToFloat puts the two 16-bit halves back together with a union. The excerpt shows the active-power read (register 0x000C) inside the three-attempt retry loop. The voltage and import-kWh reads on either side of it follow the same pattern.

static float regsToFloat(uint16_t hi, uint16_t lo) {
    union { float f; uint32_t u; } v;
    v.u = ((uint32_t)hi << 16) | (uint32_t)lo;
    return v.f;
}

// ...

bool pollMeter(MeterReading *out) {
    // ...
    const int kMaxAttempts = 3;
    for (int attempt = 1; attempt <= kMaxAttempts; attempt++) {
        if (attempt > 1) delay(50); // let the bus settle before retry

        uint8_t rc;
        // ...
        // ── Active power ─────────────────────────────────────────────────────
        rc = node.readInputRegisters(SDM_REG_POWER, 2);
        if (rc != ModbusMaster::ku8MBSuccess) {
            debugSerial.print("[app] WARN: Modbus power read error 0x");
            debugSerial.print(rc, HEX);
            if (attempt < kMaxAttempts) { debugSerial.println(" — retrying"); continue; }
            debugSerial.println();
            return false;
        }
        float pw = regsToFloat(node.getResponseBuffer(0), node.getResponseBuffer(1));
        // ...
        out->power_w    = (pw > 0.0f) ? pw : 0.0f;  // clamp export to zero
        // ...
    }
    // ...
}

8. Data Flow

Data flow: 30-s Modbus polls of V_rms/W/kWh → session state machine + offline detection → charger_alert.qo (sync:true on mains_absent) and charger_summary.qo (hourly templated) → Notehub

Collected. Every sample_interval_sec (30 s by default): V_rms, active power (W), and cumulative import kWh from the SDM120 through three Modbus RTU reads. The SDM120 does all the measurement and math itself, and the firmware reads the finished result registers.

Session tracking. The state machine opens a session when active_power_w ≥ session_threshold_w and closes it after session_end_count consecutive below-threshold wakes. At close time, the firmware computes session energy from the meter's cumulative import-kWh register:

session_kwh = import_kwh_at_close − import_kwh_at_open

That's actual metered energy, with no voltage or power-factor approximation. Window total kWh works the same way at summary time:

window_total_kwh = import_kwh_now − import_kwh_at_window_open

Availability tracking. On every wake, window_elapsed_sec grows by sample_interval_sec no matter what (wall-clock time). If meter.valid and V_rms ≥ voltage_present_v are both true, window_available_sec grows by sample_interval_sec too. When the meter can't be reached (it's unpowered, the wiring is faulted, or every Modbus retry failed), meter.valid is false. window_elapsed_sec still grows but window_available_sec doesn't, so those wakes count as unavailable in availability_pct = available_min / elapsed_min × 100. sample_coverage_pct = total_min / elapsed_min × 100 is the data-quality signal. A value of 100.0 means every poll in the window succeeded, and a lower value shows how much of the window lacked valid meter readings.

Transmitted.

  • charger_session.qo: one Note per completed session, sent immediately (sync:true). It carries metered session kWh (import-kWh delta), duration in minutes, peak active power (peak_w), the UTC epoch of the session start, and a timing_valid flag. session_kwh and peak_w are always valid. When timing_valid is false (the session opened before the first time sync), duration_min and start_epoch go out as 0, and downstream consumers must treat them as invalid. This is the record that tells you how much energy Charger 3 delivered on Tuesday at 8 a.m.
  • charger_summary.qo: one templated Note per report_interval_min (hourly by default), queued on the Notecard and sent on the outbound sync. It contains the session count, metered total kWh (import-kWh register delta using the last valid closing reading), per-session average kWh, peak active power (peak_w), active minutes (charging_min), inactive minutes (idle_min), utilization percentage, and three availability fields. Those are available_min (minutes with V_rms ≥ voltage_present_v), availability_pct = available_min / elapsed_min × 100 (a wall-clock denominator, so invalid-meter wakes count as unavailable), and sample_coverage_pct = total_min / elapsed_min × 100 (the data-quality signal). charging_min counts minutes with active power above the session threshold plus any grace-period wakes, and idle_min is the rest. Both use only valid-poll wakes as the denominator, so Modbus faults don't affect utilization_pct. available_min reflects the minutes when the charger circuit was confirmed energized. This is the record that answers both "do we need more chargers?" and "was this charger actually available to be used?"
  • charger_alert.qo: sent once if alert_offline_min passes without a mains-present V_rms reading, then suppressed until mains returns. It's a definitive signal. The SDM120 needs mains power to respond on Modbus, so consecutive failed reads at 9600 baud combined with a below-threshold V_rms reliably mean a tripped breaker, a power loss, or a sustained meter fault, not a healthy charger sitting idle.

Routed. Notehub sends the three Notefiles to separate places. charger_session.qo goes to whatever real-time channel the operator uses (fleet dashboard, CMMS work order, alerting service). charger_summary.qo goes to a long-term analytics store for trend analysis ("was March utilization higher than January?"), and charger_alert.qo goes to an on-call or ticketing endpoint.

Trigger logic summary:

  • Session opens: active_power_w ≥ session_threshold_w for one sample
  • Session closes: session_end_count consecutive below-threshold samples
  • Hourly summary: wall-clock window of report_interval_min elapsed
  • Mains-absent alert: alert_offline_min > 0 and alert_offline_min minutes elapsed since the last confirmed mains-present reading (or device commissioning if mains has never been confirmed); fires once; resets when mains returns; disabled when alert_offline_min = 0

9. Validation and Testing

Expected steady-state cadence. In normal use, a charger generates one charger_summary.qo per hour (24/day), one charger_session.qo per session (usually 1–8/day), and zero charger_alert.qo events. In a healthy window, sample_coverage_pct should be 100.0, meaning every Modbus poll succeeded. A summary that keeps showing utilization_pct above 80% means the charger is effectively saturated. If availability_pct is below 95%, the charger circuit has been losing power often and needs a maintenance visit.

Bench simulation. Before you install in a live panel, test session detection on the bench by connecting the SDM120 to a live AC circuit with a resistive load (and observe every electrical safety precaution). A resistive load of 500 W or more will exceed the default session_threshold_w = 500 W. With the SDM120 powered and the RS-485 connection wired correctly, the serial monitor should print [app] V=XXX.X V P=XXXX W kWh=X.XXX every 30 seconds. Plug the load in and watch for [app] session STARTED. Leave it running for a few minutes, then unplug it. You should count three wakes showing power near 0 W, which is the 90-second grace period (session_end_count = 3, 30 seconds each). The charger_session.qo Note queues with sync:true once the session formally closes. Give it roughly 2–3 minutes from unplugging to show up in Notehub (90 seconds of grace period + 15–60 seconds of cellular sync).

If you don't have a live SDM120 for initial bring-up, you can check the Modbus stack by connecting the RS-485 bus to a Modbus RTU simulator (e.g., a PC running Modbus Poll or a second Arduino running a Modbus slave sketch). The firmware prints [app] WARN: meter poll failed this wake for each failed read, but it still runs the Notecard initialization, env-var fetch, and sleep cycle correctly.

Using Mojo to validate power behavior. For bench bring-up, put the Mojo inline between the HDR-15-5 5V output and the Notecarrier CX +VBAT pad. Here are the published Notecard current figures from the low-power firmware design guide:

PhaseExpected current
Notecard idle (host in STOP2, radio off)~10–20 µA @ 5V (Notecard 8–18 µA + host ~1–2 µA)
Host active (Modbus poll + I²C, ~1–2 seconds per wake)Measure with Mojo (see the note below)
Notecard cellular session (LTE Cat-1 bis)~200–400 mA average, ≤2A peak bursts

The host MCU's active-phase current depends on clock configuration and peripheral state, so measure it from the Mojo trace instead of relying on an earlier estimate. A healthy trace has a near-zero baseline with brief 1–2-second elevated blips at each 30-second wake, one longer 15–30-second burst at ~200–400 mA every hour (the outbound sync), and some shorter bursts at session ends. Total daily energy depends on session count, session frequency, and cellular signal quality. Characterize it on the bench with a known load pattern, and use that measured figure as your deployment baseline.

Two failure patterns jump out on the Mojo:

  • Host never sleeping: a continuous elevated baseline instead of 30-second gaps. The host isn't entering STOP2. Check the ATTN→D5 jumper on the Notecarrier CX header (the serial log shows [app] WARN: ATTN sleep failed when ATTN never reaches D5), and confirm the firmware was built with USB support set to None (usb=none). With USB CDC enabled, the host leaves STOP2 within a millisecond.
  • Radio never going idle: long continuous transmit blocks instead of clean bursts. That means the Notecard isn't reaching its periodic mode idle, so watch the serial output to confirm the hub.set call succeeded at power-up.

Mojo is a bench-validation tool, and deployed units running from a panel supply don't need it.

10. Limitations and Next Steps

A meter-based monitor and an OCPP-integrated monitor answer different questions. This reference design deliberately answers the simpler one, which is how the circuit is being used, whatever the charger brand or network. That choice comes with trade-offs (heuristic session boundaries, no per-driver records, single-phase coverage), and they're listed below along with the natural production extensions.

Design Boundaries

These are the spots where the meter-based approach gives up authoritative session detail to stay simple and brand-agnostic. A production deployment may want to add a meter, a phase, or an OCPP back-end at any of them.

Energy-meter integration is SDM120 Modbus RTU, not OCPP. This project integrates a hardware energy meter on the charger's AC feed. It doesn't implement OCPP or any direct charger-network integration, so it can't provide per-driver, per-RFID, or charger-network session records. Session boundaries come from a power-threshold heuristic (active_power_w ≥ session_threshold_w), not authoritative OCPP StartTransaction/StopTransaction events. Sites that also need per-driver records or OCPP-correlated data should add an OCPP back-end integration alongside this meter-based monitor, as described in the production next steps at the end of this section.

SDM120 ampacity and neutral requirement. The SDM120-Modbus direct-connect variant is rated 45 A continuous, which covers 32 A EVSE (40 A breaker) and 40 A EVSE (50 A breaker). For 48 A or 60 A EVSE circuits, substitute the SDM120CT variant (it uses an external split-core CT, so its rating depends on the CT and can cover any primary current) or the SDM230-Modbus (100 A direct-connect). The SDM120 also needs a neutral connection for its internal voltage measurement. In a subpanel with only two line conductors (e.g., a 240 V line-to-line feed with no neutral), it can't measure voltage and won't work correctly. In that case, use a single-phase meter that accepts a two-wire (L-L) supply voltage input, or run a neutral conductor from the main panel.

Availability is derived from V_rms, not OCPP status. available_min and availability_pct come from the SDM120's V_rms register. When V_rms ≥ voltage_present_v, the charger circuit counts as energized. Wakes where the meter poll fails outright (because mains is absent, the wiring is faulted, or every Modbus retry fails) also count against availability. They add to the wall-clock elapsed_min denominator but not to available_min, so availability_pct correctly drops below 100 % whenever the circuit was offline for any reason. sample_coverage_pct separates sustained meter faults from genuine mains absence. If availability_pct and sample_coverage_pct are both low, the charger was probably offline. If availability_pct is low but sample_coverage_pct is near 100, the V_rms readings were valid and point to a prolonged low-voltage or brownout condition.

This design does not detect an EVSE that's powered but internally faulted, such as a GFI trip inside the EVSE, a J1772 pilot fault, or an EVSE error state. Those faults leave the circuit energized, so they look like idle time (charging_min = 0, available_min = elapsed_min). Sites that need per-charger EVSE fault detection should add OCPP status-notification integration on top of this meter monitor. Within those limits, availability_pct is still a useful signal. A sustained availability_pct < 100 in a summary window confirms a real mains interruption during that period, and availability_pct = 100 with utilization_pct = 0 means the charger was powered but unused.

Metered accuracy is Class 1, not revenue-grade. The SDM120 is IEC 62053-21 Class 1, meaning ±1% accuracy under reference conditions. That's enough for utilization monitoring and tenant cost allocation, but it falls short of the revenue-grade Class 0.5 or 0.2 instruments that utility billing requires. For billing applications, substitute a revenue-grade instrument.

Monitoring is single-phase only. This design monitors one single-phase EVSE circuit. Three-phase 208 V or 480 V commercial charging installations (typically DCFC, DC fast chargers) need a three-phase energy meter, and this reference design doesn't cover them.

Session detection is power-threshold only. The firmware can't tell "EV actively charging at full rate" from "EV plugged in but in top-off mode at reduced power." Both produce above-threshold active power and show up as one continuous session. When the BMS pauses charging, power usually drops below the threshold, so a pause longer than the session_end_count grace period (90 seconds by default) closes the session, and the resumed charge opens a new one. An OCPP-integrated data source would give you the authoritative session state.

Sessions spanning summary windows, retry-path attribution, and single-slot data loss. Window total_kwh uses the meter-register delta, so it captures all the energy in the window period regardless of session boundaries. window_sessions only counts sessions that close in the window, though, so a session still running when the summary fires gets counted in the window where it ends. On the retry path, if emitSessionNote() fails at session close and the pending retry only succeeds after the summary window has already reset, window_sessions and window_completed_session_kwh go to the newer window. Single-slot data loss: the pending-session store holds exactly one completed-session payload. If a second session closes before the first Note has been queued (a Notecard fault that lasts longer than one complete charging session), the older session record is overwritten and permanently lost, and the firmware prints a [app] WARN: overwriting unconfirmed pending session Note line to serial. Production deployments that need guaranteed per-session delivery should replace the single-slot pending store with a small ring buffer. The single slot is the pending_session_* fields of the State struct in ev_charger_session_monitor_helpers.h.

No Notecard outboard firmware update wired. The firmware doesn't configure Notecard Outboard DFU for over-the-air host firmware updates, so updating firmware takes a physical USB connection.

Production Next Steps

With the basic meter monitor running, the next steps push toward billing-grade accuracy and multi-port, OCPP-correlated coverage. They're listed roughly from most immediately useful to most dependent on outside integrations.

Upgrade to a revenue-grade meter. Swap in a single-phase Class 0.5 or 0.2 DIN-rail energy meter for utility-billing accuracy.

Add a second meter for two-port chargers. For a two-port charger (two EVSE circuits in the same panel), put a second SDM120 on the shared RS-485 bus with a different Modbus slave ID. Then extend the firmware to poll both meters and emit per-port session and summary Notes.

Implement OCPP session correlation. Connect to the charger's OCPP back-end through its WebSocket/HTTP interface or a network proxy to capture StartTransaction / StopTransaction events. Cross-referencing those records with the energy-meter session data gives you per-driver, per-RFID, per-session energy records.

Wire Notecard Outboard DFU. Connecting it to the host's boot/reset pins enables over-the-air host firmware updates across the deployed fleet.

Replace the single-slot pending store with a ring buffer. A small ring buffer (4–8 slots) in the State struct guarantees per-session delivery even through extended Notecard faults. It replaces the pending_session_* fields in ev_charger_session_monitor_helpers.h.

11. Summary

The fleet manager who used to wonder whether Bay 3 was actually being used now has the answer in Notehub, with per-session metered kWh, peak demand, session duration, port utilization, and hourly charger availability. The SDM120 measures real energy delivery and mains voltage. The Notecard dials home over LTE Cat-1 bis without IT involvement, an AP to pair with, or any per-site network setup, and session records and availability summaries start landing within minutes of commissioning.

The data answers two questions at once (how are the chargers being used and how reliably are they available), and the answers point to different problems. If a site runs at 85% utilization with 100% availability, it needs a scheduling policy rather than more hardware. A site at 72% availability needs a maintenance visit. When two chargers are maxed out while six others sit idle, that's a fleet routing problem. None of those signals was visible before, and the same SKU works on any Level 2 EVSE ever installed.

On This Page

  • 1. Project Overview
  • 2. System Architecture
  • 3. Technical Summary
  • 4. Hardware Requirements
  • 5. Wiring and Assembly
  • 6. Notehub Setup
    • What you should see in Notehub
  • 7. Firmware Design
    • 7.1 Installing and flashing
    • 7.2 Modules
    • 7.3 Sensor reading strategy
    • 7.4 Event payload design
    • 7.5 Low-power strategy
    • 7.6 Retry and error handling
    • 7.7 Key code snippet: session-end Note with immediate sync
    • 7.8 Key code snippet: sleeping until the Notecard raises ATTN
    • 7.9 Key code snippet: Modbus meter poll
  • 8. Data Flow
  • 9. Validation and Testing
  • 10. Limitations and Next Steps
    • Design Boundaries
    • Production Next Steps
  • 11. Summary

Resources

  • GitHub
  • Request More Info
Can we improve this page? Send us feedback
© 2026 Blues Inc.
© 2026 Blues Inc.
AboutDocsAPI ReferenceTermsPrivacy