EV Charger Session & Utilization Monitor

This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, feel free to reach out.
Building with an AI assistant? Point it at this accelerator's firmware — 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: for fleet managers, facilities teams, and charging-network operators who need to know how their installed chargers are actually being used. The design clamps a DIN-rail energy meter onto each charger's AC feed and reports three streams to the Blues Notehub cloud service over cellular: per-session metered kWh and peak power, hourly utilization and availability, and mains-offline alerts. Energy is measured by a hardware-metered instrument; charger availability is tracked from the meter's V_rms register — when mains voltage is absent the circuit is definitively offline. No modification to the charger hardware, no OCPP enrollment, no site IT involvement required. The hardware is a Notecarrier CX with a Notecard Cell+WiFi and an EASTRON SDM120-Modbus energy meter (see §4 for the BOM). See §10 for 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 universally, they have no idea how those chargers are actually used. Is the 48-amp circuit in Bay 3 running at 90% utilization while Bay 4 sits idle all day? Are sessions clustering in the morning rush so that employees arriving at 9 a.m. find every port occupied? Without instrumentation, all of these questions get answered by anecdote or not at all.
This project addresses the instrumentation gap directly: insert a series-wired DIN-rail energy meter (EASTRON SDM120-Modbus) on the EVSE circuit feed, poll it over Modbus RTU, and report session-level energy delivery and port utilization directly — independent of who built the charger, what network it's connected to, and whether the site team has any standing with the charging network operator. OCPP-based integrations provide richer per-driver and per-RFID records where a charger exposes that interface, but they depend on network-operator cooperation and vary across Blink, ChargePoint, EV Connect, and in-house fleet stacks. The hardware-meter approach requires none of that: it reads the physics of the circuit, and that works on any Level 2 EVSE ever installed.
What this design delivers. 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 meter is polled 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 — hardware-computed, not estimated from
current alone. Peak power (W) is the highest active-power reading seen during
the session. Charger availability is derived from the meter's V_rms register:
the firmware tracks how many minutes per reporting window the supply voltage was
above the configurable voltage_present_v threshold (available_min) and
divides that by the full wall-clock duration of the window (elapsed_min) to
produce availability_pct. Wakes where the meter is unreachable — because mains
is absent, wiring is faulted, or all Modbus retries fail — count against
availability in the denominator, so the metric correctly falls below 100 %
whenever the charger was offline for any reason. A companion
sample_coverage_pct field flags the fraction of window time that had valid
meter readings, giving a data-quality signal separate from the availability
signal. A separate mains_absent alert fires when no mains-present reading has
been seen for longer than the configurable alert_offline_min threshold: a
definitive signal for a tripped breaker, power loss, or sustained meter fault.
Sites that additionally need per-driver or per-RFID records should implement an
OCPP back-end integration alongside this meter-based monitor. See
§10 for boundaries and expansion paths.
Why Notecard. The single biggest deployment friction for EV charger instrumentation is the network question. Chargers installed in a corporate parking structure are almost always 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. The parking lot is treated as a hostile network zone: no WiFi credentials are available, no VLAN is provisioned, and no IT ticket is going to get approved by Friday. Even for in-house fleet teams, the "just connect it to the corporate network" path involves 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 sidesteps all of that. A Notecard Cell+WiFi (MBGLW) with its included prepaid SIM installs in the same electrical panel as the charger circuit, connects to an LTE Cat-1 bis tower in the parking lot, and is reporting session data within minutes — with no site IT involvement, no network form, no AP to pair to. WiFi remains as an opportunistic fallback for the rare site that has a legitimate parking-structure AP, but it is never required. The same firmware image and the same hardware SKU deploy identically in a workplace, a fleet depot, and a multi-tenant retail center.
Deployment scenario. A DIN-rail assembly mounted inside the electrical panel that feeds the EVSE circuit. The SDM120-Modbus energy meter is wired in series on the EVSE circuit (live conductor through the meter's current input); 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 are powered from a 5 V / 3 A DIN-rail supply (e.g. MeanWell HDR-15-5) fed from whatever mains voltage is present at the panel, typically 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 and handles both. Total panel footprint is four to six DIN-rail modules. No modification to the charger hardware, no OCPP enrollment, no IT coordination required.
Panel placement Note. Whether the DIN-rail assembly can be mounted inside the main EVSE panel depends on the enclosure's listing and local electrical code, not all listed enclosures permit third-party low-voltage auxiliary equipment in the mains compartment. Where the panel interior is not available, mount the assembly in a separately listed auxiliary enclosure bolted adjacent to the main panel and route the RS-485 signal cable and AC supply conductors between the two enclosures through a listed conduit entry or knockout. The auxiliary-enclosure approach is the safe default for any installation where the main panel's suitability cannot be confirmed.
2. System Architecture
Device-side responsibilities. The work 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, polls
the SDM120 over Modbus RTU for V_rms, active power, and import kWh, and runs a
two-state session machine: is the charger actively delivering energy or not?
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 is tracked the same
way each wake — 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 correctly penalizes any
wake where the circuit was offline or unreadable, instead of hiding it. When a
session closes, the host queues a charger_session.qo Note immediately with
sync:true; 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 simply stays in memory, and the host
typically stays awake less than two seconds per wake.
Notecard responsibilities. The Notecard owns the network. It queues
Notes locally, establishes the cellular (or
WiFi) session on the configured
hub.set outbound
cadence (default 60 minutes), and immediately 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 ever re-flashing firmware.
Notehub responsibilities. Once Notes leave the panel, the Notecard's
embedded global SIM carries them over supported carriers worldwide and lands
them in Notehub. Three Notefiles arrive separately so they
can take different paths downstream: charger_session.qo (per completed
session, real-time) to a utilization dashboard or fleet management system;
charger_summary.qo (periodic) to a long-term analytics store; and
charger_alert.qo (mains-absent / charger-offline alert) to an on-call
endpoint. Smart Fleets
give you the natural organization layer: one fleet per property keeps
environment-variable overrides (a site's known line voltage, say) scoped
correctly.
Routing to the cloud (high level). Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations. Route setup is project-specific and outside the scope of this reference. See the Notehub routing docs for detailed configuration.
3. Technical Summary
Before diving into the full documentation, here's the fastest path from parts to first event in Notehub:
- Create a Notehub project at notehub.io and copy its
ProductUID (looks like
com.your-company:ev-charger-monitor). - 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→D5jumper for host sleep. Full wiring details in §5. - Set your ProductUID — the sketch deliberately has no
PRODUCT_UIDplaceholder to edit; it refuses to compile (#error) until one is defined. Either add#define PRODUCT_UID "com.your-company:ev-charger-monitor"near the top offirmware/ev_charger_session_monitor/ev_charger_session_monitor.ino(above the#ifndef PRODUCT_UIDguard), or pass it as a build flag witharduino-cli(see §7.1). - Flash — compile and upload with the board options
STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none(see §7.1 for thearduino-clicommands and IDE build settings). Theusb=noneoption disables the USB CDC stack, which must be off for the host to sleep. - Watch Notehub — open your project's Events tab. You'll see
_session.qowithin seconds (proves radio works), acharger_summary.qowithin the hour, and acharger_session.qoafter you run a test load (see §9).
For detailed component selection, wiring, Notehub configuration, and validation, read on.
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
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an onboard STM32L433 host — no separate MCU needed. The host sleeps in STOP2 between samples and the Notecard's ATTN pin wakes it, for minimum idle draw. |
| Notecard Cell+WiFi (MBGLW) | 1 | LTE Cat-1 bis with global coverage and a WiFi fallback. Cellular is the primary path; see §1 for why site WiFi is rarely a viable option in EV charger deployments. Includes 500 MB data and 10 years of service on the included SIM. For technical details, see the MBGLW datasheet. |
| Blues Mojo (bench validation only) | 1 | Coulomb counter for measuring per-wake energy and validating the sleep/transmit power profile during bring-up. Not deployed to the field. See §9. |
| SparkFun CEL-16432 LTE Hinged External Antenna, 698–2700 MHz, SMA Male | 1 | Required for reliable signal inside a metal enclosure. The SMA connector screws onto the SMA female pigtail at the panel wall, placing the radiating element outside the metal cabinet. A rubber-duck antenna left inside a steel panel will degrade signal significantly. |
| SparkFun WRL-18568 SMA to U.FL Cable, 150 mm | 1 | Routes the Notecard's U.FL cellular port to the SMA female bulkhead connector in the panel wall. For panel layouts where 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 meter | 1 | Wired in series on the EVSE circuit (live conductor through the current input terminals). Measures V_rms, active power (W), and cumulative import kWh with Class 1 accuracy. RS-485 Modbus RTU interface connects to the host UART. Rated 45 A direct-connect, covering 32 A and 40 A Level 2 EVSE; for 48 A or 60 A EVSE circuits use the SDM120CT variant with an appropriate 5 A secondary split-core CT, or the SDM230-Modbus (100 A direct). The SDM120 requires both a live conductor and a neutral terminal for voltage sensing — confirm both are accessible at the panel before specifying this meter. The Modbus protocol document (register map, baud-rate defaults, function codes) is available as a PDF download after free registration on the Eastron Europe website. |
| SparkFun RS-485 Transceiver Breakout – SP3485 (BOB-10124) | 1 | Converts the host's 3.3 V UART to the RS-485 differential pair required by the SDM120. The SP3485 chip operates from a single 3.3 V supply, eliminating logic-level compatibility concerns that arise 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 no separate jumper is needed. VCC to +3V3, GND to GND. |
| AC/DC DIN-rail supply, 85–264VAC input, 5 V / 2.4 A output (e.g. MeanWell HDR-15-5) | 1 | Derives 5 V DC from whatever mains voltage is present at the panel (120 VAC L-N or 208/240 VAC L-L). The 2.4 A output rating provides adequate transient headroom for Notecard cellular transmit bursts (≤2 A peak). DIN-rail mount integrates cleanly into the panel enclosure alongside the EVSE breaker. |
| DIN-rail enclosure, ≥6-module width | 1 | Houses 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 including 500 MB of data and 10 years of service — no activation fees, no monthly commitment.
5. Wiring and Assembly
Electrical safety. The 120/240VAC feed inside an electrical panel is hazardous. This installation inserts the SDM120 energy meter in series on the EVSE feed, which requires interrupting and re-terminating the live mains conductors. Panel work must be performed by a qualified electrician following applicable electrical codes and lockout/tagout procedures.
Panel placement. Mounting third-party low-voltage electronics inside a mains EVSE panel is subject to the enclosure's UL/CSA listing and local electrical code. Where the panel listing or jurisdiction does not permit auxiliary equipment in the mains compartment, install the Notecarrier CX, power supply, and RS-485 transceiver module in a separately listed auxiliary enclosure mounted adjacent to the main panel. Route the RS-485 signal cable and AC supply conductors between the two enclosures through a listed conduit entry or knockout. The auxiliary-enclosure approach is the safe default for any installation where the main panel's interior cannot be confirmed suitable.
SDM120 meter installation. The SDM120-Modbus is a series element: the EVSE circuit live conductor must 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 — 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 do not rely on a generic reference for this wiring. Connect the neutral conductor to the meter's neutral terminal — the SDM120 requires a neutral connection for its internal voltage measurement. Do not route the neutral through the current input. The SDM120 derives its own supply from the same line and neutral conductors feeding the EVSE; consult the datasheet for the exact supply terminal connections on your variant. This installation interrupts the EVSE circuit during wiring; follow lockout/tagout procedures and have a qualified electrician perform this work.
Circuit ampacity. The SDM120-Modbus is rated 45 A continuous. It is suitable for 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 an appropriately rated split-core CT, or the SDM230-Modbus (100 A rated). See §10 for substitution guidance.
RS-485 wiring. Connect the SDM120's RS-485 terminals (A+ and B−) to the corresponding A and B terminals on the SparkFun BOB-10124 breakout (via the 3.5 mm screw terminal or 0.1" header) using a twisted-pair cable (120 Ω characteristic impedance recommended). 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 transmitting a Modbus request frame and LOW immediately after, switching 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 reduces reflections; at the short cable lengths typical in a panel enclosure (< 1 m) it is optional.
Notecard and antenna. Seat the Notecard Cell+WiFi (MBGLW) into 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 degrade signal significantly. The Notecard communicates with the STM32 host over I²C on the internal Notecarrier bus; no additional wiring is needed for that path.
ATTN-to-D5 jumper (host sleep). Fit a short jumper between the ATTN and
D5 pins on the Notecarrier CX header — both are 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; 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 rather
than sleep the host. 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 — 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 both configurations. Consult 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,
insert 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 —
see §9 for 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
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID — it looks like
com.your-company.your-name:ev-charger-monitor. Define it asPRODUCT_UID— either add#define PRODUCT_UID "com.your-company:ev-charger-monitor"above the#ifndef PRODUCT_UIDguard infirmware/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#errorifPRODUCT_UIDis undefined, so a misconfigured binary can't be flashed by accident; for bench work without a Notehub project yet,-DALLOW_EMPTY_PRODUCT_UIDsuppresses the error (never ship a build with that flag). -
Claim the Notecard. Power the assembled unit. On first cellular connect the Notecard associates with your Notehub project automatically. The device will appear in Devices within a minute or two.
-
Create a Fleet per property. Fleets group devices for shared configuration and routing. One fleet per property or tenant is the natural unit here — every charger at a given site shares the same line voltage, the same session-detection thresholds, and the same routing destinations. Smart Fleets can auto-assign new devices to the correct fleet at commissioning time; within a single Notehub project all devices share the same ProductUID, so fleet rules should discriminate on a device attribute that varies by site — for example, an installer-applied tag (e.g.,
site:depot-north), a serial/UID range reserved per property during provisioning, or location metadata populated during the first sync. -
Set environment variables. In Notehub, navigate to Fleet → Environment tab (or Device → Environment for a per-device override). Add or update any of the variables below; the device will fetch them on its next inbound sync — no reflash needed. All variables are optional; the firmware defaults shown below apply if you don't set them.
Variable Default Purpose sample_interval_sec30Seconds between host wakes and Modbus meter polls. report_interval_min60Minutes between charger_summary.qoNotes. Changing this also re-applieshub.setso the Notecard's outbound cadence stays in sync.session_threshold_w500Active power (W) above which a charging session is considered open. Level 2 EVSE minimum pilot current (6 A × 208 V) is ~1250 W; 500 W sits safely below any real session while above charger standby draw. session_end_count3Consecutive below-threshold wakes before a session is closed. At the default 30-second interval, this is a 90-second grace period that absorbs 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_mintimer. 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.qoNote fires. Set to0to disable offline alerting entirely.modbus_slave_id1Modbus slave address of the SDM120. The SDM120 factory default is 1; change if you have multiple meters on the same bus. modbus_baud9600Modbus RTU baud rate. Must match the SDM120's configured baud (factory default is 2400 on older units, 9600 on most current stock — check your meter's display or documentation). -
Configure routes. Add at minimum one route for
charger_session.qo(real-time delivery to a fleet management system or dashboard webhook) and a second forcharger_summary.qo(long-term analytics or historian). For sites where the mains-absent alert matters, add a third route forcharger_alert.qoto an on-call or ticketing endpoint. The three Notefiles are separate specifically so each can be fanned out at a different urgency without filter logic in the route.
What you should see in Notehub
Three event types matter:
_session.qo— automatic Notecard housekeeping on each cellular session. Confirms the radio is reaching Notehub.charger_summary.qo— one perreport_interval_min(default hourly). 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_kwhis the SDM120's import-kWh register delta: the most recently valid closing reading minus the register value when the window opened, which is actual metered energy, not an estimate. If the meter poll fails on the exact wake when the summary fires, the most recent previously-valid reading is used as the closing value, sototal_kwhremains accurate and the next window's kWh baseline is not corrupted.charging_min+idle_minalways equalstotal_minby construction.total_mincounts only wakes in which the meter returned valid data; wakes where all Modbus retries failed are excluded from the utilization accumulators so transient bus faults do not inflate idle time or distortutilization_pct.utilization_pct=charging_min / total_min × 100.available_minis the minutes during which V_rms was ≥voltage_present_v;availability_pct=available_min / elapsed_min × 100, whereelapsed_minis the full wall-clock window duration, including wakes where the meter was unreachable, which count as unavailable.sample_coverage_pct=total_min / elapsed_min × 100and signals the fraction of wall-clock window time that had valid meter readings; 100.0 means every poll succeeded, and a lower value flags Modbus or mains downtime during the window. In a fully-energized window where all polls succeeded and the charger was in use for 38 of 60 minutes:available_min= 60,availability_pct= 100.0,sample_coverage_pct= 100.0, andutilization_pct= 63.3.charger_session.qo— one per completed charging session, emitted immediately (sync:true). Sample body:{ "session_kwh": 11.4, "duration_min": 63.5, "peak_w": 10320.0, "start_epoch": 1746283200, "timing_valid": true }session_kwhis the SDM120's import-kWh delta (close reading minus open reading): actual metered energy for this session.peak_wis the highest active-power reading seen during the session. Both fields are always valid regardless oftiming_valid.timing_validisfalse— andduration_minandstart_epochare emitted as0— when a session was opened before the Notecard completed its first cellular time sync. Suppress or specially flag session records withtiming_valid: falsein downstream analytics;session_kwhandpeak_wcan still be trusted for those records.charger_alert.qo— emitted once (then suppressed until mains returns) whenalert_offline_minminutes elapse without a mains-present V_rms reading. The timer reference is the last confirmed mains-present epoch, or (if mains has never been confirmed) the device's first valid epoch after commissioning, so the alert fires even for a circuit that has 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—setup(),loop(), and top-level app flow (one-time init, per-wake Modbus meter poll, sleep scheduling).ev_charger_session_monitor_helpers.h— shared constants, theStatestruct, extern declarations, and function prototypes.ev_charger_session_monitor_helpers.cpp— all helper implementations (Modbus polling, session state machine, Note emission, env-var handling, host sleep).cx_sleep.h— host sleep for the Notecarrier CX: thecard.attnrequest, STOP2 entry, andATTN-pin wake.
7.1 Installing and flashing
Dependencies:
- Arduino core for STM32 —
stm32duino/Arduino_Core_STM32. Add the board manager URLhttps://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.jsonunder File → Preferences → Additional Boards Manager URLs. The Notecarrier CX's host uses the Cygnet board definition (see build settings below). Blues Wireless Notecardlibrary —note-arduino. Install via the Arduino Library Manager (arduino-cli lib install "Blues Wireless Notecard") or search "Blues Wireless Notecard" in the IDE Library Manager. See the note-arduino releases page for any newer stable version.ModbusMasterlibrary — by Doc Walker, available in the Arduino Library Manager. Install witharduino-cli lib install "ModbusMaster"or search "ModbusMaster" in the IDE. Version ≥ 2.0.1. Provides the Modbus RTU master implementation used to communicate with the SDM120 over Serial1.STM32duino Low Power(and its dependencySTM32duino RTC) —stm32duino/STM32LowPower. Install witharduino-cli lib install "STM32duino Low Power" "STM32duino RTC"or via the IDE Library Manager. Provides the STOP2 entry andATTN-pin wake used incx_sleep.h.
Build settings: the Notecarrier CX's host is the same STM32L433 as the Blues
Cygnet, so the board is Blues boards → Cygnet (pnum=CYGNET), with USB
support (if available) set to None (usb=none). With the USB CDC stack
enabled and no USB host attached (or the Notecarrier CX USB-C switch in NC),
the STM32's USB wakeup interrupt fires continuously and the host never stays in
STOP2. Debug output goes to the LPUART on the Notecarrier CX debug jack instead
— an ST-LINK V3 exposes it as a virtual COM port; see Serial Logging With
STLINK. Serial1 on D0/D1 stays
dedicated to the RS-485 link.
Flashing — Arduino IDE: open ev_charger_session_monitor.ino, apply the
build settings above, hit Upload. With the Notecarrier CX's USB-C switch in
HST, the host's DFU bootloader is reachable over the USB-C port (hold BOOT,
tap RST); alternatively upload over SWD with an ST-LINK on the debug jack.
Flashing — arduino-cli: from the firmware directory,
# 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 —
typically COMx on Windows, /dev/ttyACM* on Linux, or /dev/cu.usbmodem* on
macOS.
Open a serial monitor at 115200 baud on the ST-LINK virtual COM port after
flashing. On power-up you'll see [app] power-up — will configure Notecard.
Once the meter is connected, subsequent wakes print
[app] V=240.1 V P=0 W kWh=12.345 and then go quiet for 30 seconds as the
host sleeps in STOP2. If the meter is absent, unpowered, or wired incorrectly
you'll see [app] WARN: Modbus voltage read error 0xE2 (response timeout) each
wake. A 0x02 error indicates the slave responded but rejected the register
address (typical of a slave-ID or register-map mismatch); other ModbusMaster
error codes are documented in the library header.
7.2 Modules
| Responsibility | Function |
|---|---|
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 fetch | fetchEnvOverrides |
| SDM120 Modbus RTU poll (V_rms, active power, import kWh) | pollMeter |
| Epoch time from Notecard | getEpoch |
| Session state machine (IDLE ↔ CHARGING) with availability tracking | runSessionStateMachine |
| Per-session Note emission | emitSessionNote |
| Hourly summary Note emission and window reset | emitSummaryNote |
| Mains-absent alert emission | emitOfflineAlert |
Host sleep (STOP2 until the Notecard raises ATTN) | sleepHost → cxSleepUntilAttn (cx_sleep.h) |
7.3 Sensor reading strategy
pollMeter issues three sequential Modbus RTU read transactions (Function Code
0x04 — input registers) to the SDM120, one for each required parameter: 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 is sufficient for the SDM120 to respond at 9600 baud (a full 9-byte
response frame takes under 10 ms). If any read returns a non-zero Modbus error
code, pollMeter retries the complete three-register sequence up to three times
(50 ms inter-attempt pause). This absorbs the single-frame bus collisions and
turnaround-timing faults that most commonly occur at power-on or after a brief
conductor disturbance. If all three attempts fail (indicating the meter is
unpowered, wiring is faulty, or the slave ID is misconfigured), pollMeter
returns valid = false. When that happens, runSessionStateMachine suppresses
all session-state transitions and all window accumulation (charging_sec,
idle_sec) for that wake: a session in progress is neither extended nor closed
(its below_threshold_count is not incremented), and no idle time is attributed
to the failed sample. However, window_elapsed_sec still advances
unconditionally, so the failed poll counts against availability_pct (as an
unavailable interval) and reduces sample_coverage_pct. The mains-absent
offline timer continues to advance independently (via last_mains_epoch in
loop()), so sustained meter faults still trigger the mains_absent alert
after alert_offline_min minutes.
The SDM120 performs all V and I measurement and computation internally; the host reads the already-computed result registers. No ADC sampling, no voltage or power-factor assumptions, and no analog front-end components are required on the Notecarrier side.
7.4 Event payload design
charger_summary.qo uses a
template
that encodes each record as ~40 bytes on the wire (versus ~200 bytes as
free-form JSON). At one record per hour per charger across a 50-unit fleet, this
is meaningful over the lifetime 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 intentionally left untemplated.
Session Notes are infrequent (one per EV charge, typically 1–8 per day) and the
alert body has a different shape from the session body, so keeping them
untemplated avoids the schema-conflict issue that arises from mixing two
different body shapes under a single template.
Template definition snippet:
J *req = notecard.newRequest("note.template");
JAddStringToObject(req, "file", FILE_SUMMARY);
JAddNumberToObject(req, "port", 50);
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "sessions", 12); // 2-byte signed int
JAddNumberToObject(body, "total_kwh", 14.1); // 4-byte IEEE-754 float — metered
JAddNumberToObject(body, "avg_session_kwh", 14.1);
JAddNumberToObject(body, "peak_w", 14.1); // measured peak active power
JAddNumberToObject(body, "charging_min", 12);
JAddNumberToObject(body, "idle_min", 12);
JAddNumberToObject(body, "utilization_pct", 14.1);
JAddNumberToObject(body, "available_min", 12); // minutes with mains present
JAddNumberToObject(body, "availability_pct", 14.1); // available_min / elapsed_min × 100
JAddNumberToObject(body, "sample_coverage_pct", 14.1); // total_min / elapsed_min × 100
notecard.sendRequest(req);Template notation explained: 12 means a 2-byte signed integer (range
-32,768 to 32,767); 14.1 means a 4-byte IEEE-754 float with 1 decimal place in
transmission. The template compresses each full summary record from roughly 200
bytes of free-form JSON to about 40 bytes on the wire, meaningful over a 500 MB
SIM lifetime at one record per hour per device.
7.5 Low-power strategy
The device is grid-tied, so absolute energy budget is not critical, but a
well-disciplined sleep pattern still matters: it keeps the enclosure cooler,
reduces component stress, and creates a firmware pattern that can be ported
directly to a battery- or solar-backed variant without a rewrite. After each
30-second wake, sleepHost() calls cxSleepUntilAttn() (in cx_sleep.h),
which sends a card.attn sleep request for sample_interval_sec seconds,
waits for the Notecard to pull ATTN low, and puts the STM32 host into STOP2,
where it draws a few microamps with its RAM intact. When the interval elapses
the Notecard raises ATTN; the rising edge on D5 wakes the host and execution
resumes on the next line of loop(). Because the host resumes in place rather
than cold-booting, the State struct — session baseline, window accumulators,
pending Note, cached config — simply 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 (jumper missing), sleepHost() waits out the interval
awake and tries again on the next cycle, so the 30-second cadence is preserved
either way. The Notecard sits in its 8–18 µA idle between cellular sessions. 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 every 30 seconds, and the radio runs roughly once an hour plus any
session-end sync events.
hub.set is configured for periodic mode with outbound matching
report_interval_min (default 60 min) and inbound at 120 min. Session end
Notes set sync:true, which causes the Notecard to open a cellular session
immediately rather than waiting for the outbound timer. With the default
session_end_count=3 and a 30-second sample interval, the state machine
requires 3 consecutive below-threshold wakes (a 90-second grace period) before
formally closing the session and queuing the Note. Cellular sync then adds
roughly 15–60 seconds. End-to-end, operators should expect a completed session
to appear in Notehub roughly 2–3 minutes after the car unplugs with default
settings. Reducing session_end_count (e.g., to 1) shortens the grace period
at the cost of increased false-close risk during EV charge-phase transitions.
7.6 Retry and error handling
pollMeterretries the complete three-register Modbus sequence up to three times per wake before declaring a poll failure. When all retries fail,runSessionStateMachinesuppresses all session-state transitions and all window accumulation (charging_sec,idle_sec) for that wake — a session already in progress is held open (itsbelow_threshold_countis not incremented), and no idle time is attributed to the failed sample.window_elapsed_secstill advances (as it does every wake), so the failed poll counts againstavailability_pctin the denominator and also reducessample_coverage_pct. The mains-absent offline timer continues to advance regardless, so a sustained meter fault still triggers themains_absentalert afteralert_offline_minminutes.fetchEnvOverridespasses the storedenv_last_modifiedtimestamp toenv.get'stimefilter. If nothing has changed since the last fetch, the Notecard returns an empty response body, and the function returns immediately — no parsing, no config updates, minimal I²C overhead.getEpochreturns 0 if the Notecard hasn't yet synced and obtained time. The summary Note is guarded bynow > 0andwindow_start_epoch > 0, so it does not fire before a cellular session establishes. The mains-absent alert is guarded byoffline_ref > 0, whereoffline_refis the last mains-present epoch, orwindow_start_epochwhen mains has never been confirmed — so the alert is suppressed until the device has a valid clock and the configured absence duration has genuinely elapsed.emitSessionNotesetstiming_valid = falsewhen thestart_epochparameter is 0 — which happens when a session was opened before the Notecard completed its first cellular time sync. In that caseduration_minandstart_epochwould be wildly incorrect, so both are emitted as 0 andtiming_valid: falseis included in the Note body to signal suppression to downstream consumers.session_kwhandpeak_ware unaffected and always emitted normally.- Env var changes to
report_interval_minre-applyhub.setviaapplyHubCadence(). Thehub_cadence_dirtyflag is set before the attempt and cleared only on confirmed success, so a single transient I²C failure cannot leave the Notecard permanently on the old outbound cadence;loop()retries on every subsequent wake until the cadence is confirmed updated — mirroring thenotecard_configuredretry pattern. - When
emitSessionNote()fails at session end, the completed payload is stored in a dedicated pending-note struct (pending_session_*) andsession_activeis cleared immediately. The pending note is retried at the top ofrunSessionStateMachine()on each subsequent wake, but the function always falls through to normal state-machine processing regardless of whether the retry succeeded — so a back-to-back charging session during a transient Notecard fault is detected and opened correctly rather than 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 subsequent successful retry, the session's count and completed-session kWh are attributed to the newer window rather than the window in which the session actually ended. This edge case is documented further in §10. alert_offline_minaccepts0(set the env var to0to disable mains-absent alerting), or any positive integer to adjust the threshold. The firmware checks for key presence before applying the value, so an absent env var leaves the current setting unchanged.
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. All payload values are passed as explicit parameters
so the function works identically at first-close time and during pending-note
retries — after session state has been cleared and possibly overwritten by a new
session. A timing_valid guard protects against sessions opened before the
Notecard had a valid epoch: if start_epoch is 0, duration_min and
start_epoch would be wildly incorrect, so they are emitted as 0 and
timing_valid is set to false. session_kwh and peak_w are always valid
regardless.
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); // metered import-kWh delta
JAddNumberToObject(body, "duration_min", (float)dur_sec / 60.0f);
JAddNumberToObject(body, "peak_w", peak_w); // measured peak active power
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. The host stays alive, so the card.attn call is a request whose
result is checked rather than a fire-and-forget command. STOP2 retains RAM, so
the session start kWh baseline, peak power, and hourly window state are simply
still there when loop() runs again.
// In setup() — arm the ATTN wake once
cxSleepBegin();
// At the end of every loop() pass — sleep until ATTN rises
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; regsToFloat reassembles the two 16-bit halves via a
union.
// Read active power (0x000C, 2 registers → float32)
uint8_t rc = node.readInputRegisters(SDM_REG_POWER, 2);
if (rc != ModbusMaster::ku8MBSuccess) {
Serial.print("[app] WARN: Modbus power read error 0x");
Serial.println(rc, HEX);
return false;
}
float pw = regsToFloat(node.getResponseBuffer(0), node.getResponseBuffer(1));
out->power_w = (pw > 0.0f) ? pw : 0.0f; // clamp export to zero
// Helper: reassemble two 16-bit Modbus registers into IEEE-754 float
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;
}8. Data Flow
Collected. Every sample_interval_sec (default 30 s): V_rms, active power
(W), and cumulative import kWh from the SDM120 via three Modbus RTU reads. The
SDM120 performs all measurement and computation internally; the firmware reads
the already-computed 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. Session energy is computed at close time from
the meter's cumulative import-kWh register:
session_kwh = import_kwh_at_close − import_kwh_at_openThis is actual metered energy — no voltage or power-factor approximation. Window total kWh is computed the same way at summary time:
window_total_kwh = import_kwh_now − import_kwh_at_window_openAvailability tracking. Each wake, window_elapsed_sec grows by
sample_interval_sec unconditionally (wall-clock time). Additionally, if
meter.valid and V_rms ≥ voltage_present_v, window_available_sec also grows
by sample_interval_sec. When the meter is unreachable (meter unpowered, wiring
fault, or all Modbus retries failed), meter.valid is false:
window_elapsed_sec still grows but window_available_sec does not, so those
wakes count as unavailable in availability_pct =
available_min / elapsed_min × 100. sample_coverage_pct =
total_min / elapsed_min × 100 provides a data-quality signal — 100.0 means
every poll in the window succeeded; a lower value quantifies how much of the
window lacked valid meter readings.
Transmitted.
charger_session.qo— one Note per completed session, emitted immediately (sync:true), carrying metered session kWh (import-kWh delta), duration in minutes, peak active power (peak_w), UTC epoch of session start, and atiming_validflag.session_kwhandpeak_ware always valid. Whentiming_validisfalse(session opened before the first time sync),duration_minandstart_epochare emitted as 0 and must be treated as invalid by downstream consumers. This is the record that answers "how much energy did Charger 3 deliver on Tuesday at 8 a.m.?"charger_summary.qo— one templated Note perreport_interval_min(default hourly), queued to the Notecard and transmitted on the outbound sync. Contains 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 availability fields:available_min(minutes with V_rms ≥voltage_present_v),availability_pct=available_min / elapsed_min × 100(wall-clock denominator so invalid-meter wakes count as unavailable), andsample_coverage_pct=total_min / elapsed_min × 100(data-quality signal).charging_mincounts minutes with active power above the session threshold plus any grace-period wakes;idle_minis the remainder; both use only valid-poll wakes as the denominator soutilization_pctis unaffected by Modbus faults.available_minreflects the minutes where 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— emitted once ifalert_offline_minelapses without a mains-present V_rms reading, then suppressed until mains returns. This is a definitive signal: the SDM120 requires mains power to respond on Modbus, so consecutive failed reads at9600baud combined with a below-threshold V_rms reliably indicate a tripped breaker, power loss, or sustained meter fault rather than a healthy charger sitting idle.
Routed. Notehub fans the three Notefiles out separately:
charger_session.qo to whatever real-time channel the operator uses (fleet
dashboard, CMMS work order, alerting service), charger_summary.qo to a
long-term analytics store for trend analysis ("was March utilization higher than
January?"), and charger_alert.qo to an on-call or ticketing endpoint.
Trigger logic summary:
- Session opens:
active_power_w ≥ session_threshold_wfor one sample - Session closes:
session_end_countconsecutive below-threshold samples - Hourly summary: wall-clock window of
report_interval_minelapsed - Mains-absent alert:
alert_offline_min> 0 andalert_offline_minminutes elapsed since the last confirmed mains-present reading (or device commissioning if mains has never been confirmed); fires once; resets when mains returns; disabled whenalert_offline_min = 0
9. Validation and Testing
Expected steady-state cadence. In normal use, a charger with two or three
sessions per day generates: one charger_summary.qo per hour (24/day), one
charger_session.qo per session (2–8/day), and zero charger_alert.qo events.
In a healthy window, sample_coverage_pct should be 100.0 (every Modbus poll
succeeded). If the summary Note shows utilization_pct consistently above 80%,
the charger is effectively saturated; if availability_pct is below 95%, the
charger circuit has been losing power frequently and maintenance is warranted.
Bench simulation. Before installing in a live panel, test session detection
on the bench by connecting the SDM120 to a live AC circuit (observe all
electrical safety precautions) with a resistive load. A 500 W or larger
resistive load will exceed the default session_threshold_w = 500 W. With the
SDM120 powered and the RS-485 connection wired correctly, you should see the
serial monitor printing [app] V=XXX.X V P=XXXX W kWh=X.XXX every 30 seconds.
Plug the load in; watch the monitor print [app] session STARTED. Leave it
running for a few minutes, then unplug. Count three wakes printing power near 0
W — 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; allow roughly 2–3 minutes from unplugging for the Note to appear in
Notehub (90 seconds grace period + 15–60 seconds cellular sync).
If you do not have a live SDM120 available for initial bring-up, you can verify
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 will print [app] WARN: meter poll failed this wake for each failed
read but will still run the Notecard initialization, env-var fetch, and sleep
cycle correctly.
Using Mojo to validate power behavior. Insert the
Mojo inline between the HDR-15-5 5V output and
the Notecarrier CX +VBAT pad for bench bring-up. Published Notecard current
figures from the low-power firmware design
guide:
| Phase | Expected 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 Note below |
| Notecard cellular session (LTE Cat-1 bis) | ~200–400 mA average, ≤2A peak bursts |
The host MCU active-phase current depends on clock configuration and peripheral state; use the Mojo trace to measure it rather than relying on a prior estimate. A healthy Mojo trace looks like this: a near-zero baseline punctuated by brief 1–2-second elevated blips at each 30-second wake, with one longer 15–30-second burst at ~200–400 mA every hour (the outbound sync) plus additional shorter bursts at session ends. Total daily energy varies with 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 are immediately visible on the Mojo:
- Host never sleeping: continuous elevated baseline instead of 30-second
gaps. The host is not entering STOP2 — check the
ATTN→D5jumper on the Notecarrier CX header (the serial log shows[app] WARN: ATTN sleep failedwhenATTNnever reachesD5) 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 rather than clean
bursts. Indicates the Notecard is not entering
periodicmode idle — verify thehub.setcall succeeded at power-up by watching serial output.
Mojo is bench-validation tooling — 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, and this reference design deliberately answers the simpler one: how is the circuit being used, regardless of charger brand or network. The trade-offs that come with that choice (heuristic session boundaries, no per-driver records, single-phase coverage) are listed below, alongside the natural production extensions.
Design Boundaries
Each boundary below is a place where the meter-based approach trades authoritative session detail for brand-agnostic simplicity, and where a production deployment may want to add a meter, a phase, or an OCPP back-end.
Energy-meter integration is SDM120 Modbus RTU, not OCPP. This project
implements a hardware energy-meter integration on the charger's AC feed. It does
not implement OCPP or direct charger-network integration and therefore does not
provide per-driver, per-RFID, or charger-network session records. Session
boundaries are power-threshold heuristics
(active_power_w ≥ session_threshold_w), not authoritative OCPP
StartTransaction/StopTransaction events. Sites that additionally require
per-driver records or OCPP-correlated data should implement an OCPP back-end
integration alongside this meter-based monitor. See 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. It 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 (uses an external split-core CT; rated for any primary current, CT-dependent) or the SDM230-Modbus (100 A direct-connect). Additionally, the SDM120 requires a neutral connection for its internal voltage measurement. In a subpanel where only two line conductors are available (e.g., a 240 V line-to-line feed with no neutral), the SDM120 cannot measure voltage and will not operate correctly; use a single-phase meter that accepts a two-wire (L-L) supply voltage input instead, or run a neutral conductor from the main panel.
Availability is derived from V_rms, not OCPP status. available_min and
availability_pct are derived from the SDM120's V_rms register. When V_rms ≥
voltage_present_v, the charger circuit is classified as energized. Wakes where
the meter poll fails entirely (because mains is absent, wiring is faulted, or
all Modbus retries fail) also count against availability: they contribute to the
wall-clock elapsed_min denominator but not to available_min, so
availability_pct correctly falls below 100 % whenever the circuit was offline
for any reason. sample_coverage_pct distinguishes sustained meter faults from
genuine mains absence: if availability_pct is low and sample_coverage_pct is
also low, the charger was likely offline; if availability_pct is low but
sample_coverage_pct is near 100, the V_rms readings were valid and indicate a
prolonged low-voltage or brownout condition. This design does not detect an
EVSE that is powered but internally faulted (e.g., a GFI trip within the EVSE, a
J1772 pilot fault, or an EVSE error state); those faults leave the circuit
energized and appear as idle time
(charging_min = 0, available_min = elapsed_min). Sites requiring per-charger
EVSE fault detection should supplement this meter monitor with OCPP
status-notification integration. Within those limits, availability_pct
provides a useful signal: sustained availability_pct < 100 in a summary window
confirms a real mains interruption during that period, and
availability_pct = 100 with utilization_pct = 0 indicates 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. This is sufficient for utilization monitoring and tenant cost allocation, but falls short of revenue-grade Class 0.5 or 0.2 instruments required for utility billing. 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) require a three-phase energy meter and are not covered by this reference design.
Session detection is power-threshold only. The firmware does not distinguish between "EV actively charging at full rate," "EV plugged in but in top-off mode at reduced power," and "EV plugged in but the BMS has paused charging." All three produce above-threshold active power and appear as a single continuous session. An OCPP-integrated data source would provide the authoritative session state.
Sessions spanning summary windows, retry-path attribution, and single-slot
data loss. Window total_kwh uses the meter-register delta and captures all
energy in the window period regardless of session boundaries. However,
window_sessions counts only sessions that close in the window — a session
still in progress when the summary fires will be 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 are attributed to the newer
window. Single-slot data loss: the pending-session store holds exactly one
completed-session payload. If a second session closes while the first Note has
not yet been successfully queued (Notecard fault lasting longer than one
complete charging session), the older session record is overwritten and
permanently lost. A [app] WARN: overwriting unconfirmed pending session Note
line is printed to serial. Production deployments requiring guaranteed
per-session delivery should replace the single-slot pending store with a small
ring buffer. See the code comment in ev_charger_session_monitor_helpers.h.
No Notecard outboard firmware update wired. The firmware does not configure Notecard Outboard DFU for over-the-air host firmware updates. Firmware updates require a physical USB connection.
Production Next Steps
Once the basic meter monitor is running, these extensions move it toward billing-grade accuracy and multi-port, OCPP-correlated coverage, roughly from the most immediately useful to the most integration-dependent.
Upgrade to a revenue-grade meter. Swap in a Class 0.5 or 0.2 DIN-rail energy meter (e.g., EASTRON SDM72D or equivalent) for utility-billing accuracy.
Add a second meter for two-port chargers. For a two-port charger (two EVSE circuits in the same panel), add a second SDM120 on a shared RS-485 bus using a different Modbus slave ID; extend the firmware to poll both meters and emit per-port session and summary Notes.
Implement OCPP session correlation. Integrate with the charger's OCPP
back-end via its WebSocket/HTTP interface or a network proxy to capture
StartTransaction / StopTransaction events, then cross-reference those
records with energy-meter session data to get per-driver, per-RFID, per-session
energy records.
Wire Notecard Outboard DFU. Connect it to the host's boot/reset pins for 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
during extended Notecard faults. See the extension Notes 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: per-session metered kWh, peak demand, session duration, port utilization, and charger availability, hourly. The SDM120 measures real energy delivery and mains voltage; the Notecard dials home over LTE Cat-1 bis with no IT involvement, no AP to pair to, and no per-site network setup. Session records and availability summaries start landing within minutes of commissioning. The signal that emerges answers two questions at the same time (how are the chargers being used and how reliably are they available) and the answers cleanly separate the problems they imply. A site running at 85% utilization with 100% availability needs a scheduling policy, not more hardware. A site at 72% availability needs a maintenance visit. A site where two chargers are maxed out while six others sit idle has 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
- 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
- 11. Summary