06 · Examples
This chapter opens with the flagship scenarios — the real deployment patterns the library was designed for (mains + N sub-loads on a single bus, GC-synchronized snapshots). They are followed by single-module scenarios — from a minimal "hello world" to a battery-powered deep-sleep logger.
Most scenarios ship as a ready-to-build IDF project in
examples/, which you can build with
idf.py -C examples/<name> build flash monitor. The code in this
chapter is a distillation of the key logic, not the full project
(bus_cfg / WiFi bring-up / Kconfig are omitted for brevity).
| # | Scenario | Difficulty | Source |
|---|---|---|---|
| 1 | Mains + N sub-loads — the 80% canon | medium | examples/fleet_sync/ |
| 2 | Provisioning workflow (virgin → fleet) | low | examples/provision/ |
| 3 | Multi-channel mixed-CT (I3, different models) | medium | examples/multi_channel/ |
| 4 | Fleet GC sync — billing-grade synchronicity | medium | examples/fleet_sync/ |
| 5 | Quick read (single module) | minimal | examples/quick_read/ |
| 6 | Wh meter on a 16×2 LCD | low | examples/lcd_period/ |
| 7 | Monitoring 3 modules (legacy sequential) | low | examples/multi_module/ |
| 8 | MQTT per channel | medium | examples/mqtt_publisher/ |
| 9 | Bidirectional metering on the master side | medium | (composition) |
| 10 | Whole-home energy balance (3 modules) | high | (composition) |
| 11 | Event-based logging (EMA) | medium | (composition) |
| 12 | Local CSV logger on SPIFFS | medium | examples/spiffs_logger/ |
| 13 | Battery logger with deep-sleep | medium | examples/deep_sleep_logger/ |
| 14 | I3 with different CT per channel (mixed-load deep-dive) | medium | (composition) |
| 15 | Sub-metering: 5×UI1 on a shared bus | high | (composition) |
| 16 | GC broadcast latch for >8 modules | high | (composition) |
| 17 | GC group filtering (multi-tenant billing) | high | (composition) |
Scenarios 1–4 are the flagship ones, built around the library's canonical deployment. Their code uses the fleet API and is validated on bench hardware (a heterogeneous UI1+I2+I3 fleet). Scenarios 5–13 are single-module and compose patterns; 14–17 are extended fleet scenarios (a deep-dive into the capabilities introduced in 1–4). Scenario 7 is the legacy sequential per-module latch without the fleet API, kept for comparison and as a migration path.
All scenarios assume the sensor class and CT model have already been configured via
rbamp_set_sensor_class()+rbamp_set_ct_model()(see 05 · Quickstart Step 4). This is done once at installation time — the settings are stored in the module's flash and survive a reset.
Extra REQUIRES for main/CMakeLists.txt
The base component pulls in esp_driver_i2c, esp_timer, log.
Some scenarios use additional ESP-IDF components — add them to the
REQUIRES of your main/:
| Scenario | Extra REQUIRES |
|---|---|
| 1 — Mains + N sub-loads (fleet) | — (rbamp only) |
| 2 — Provisioning workflow | — |
| 3 — Multi-channel mixed-CT (I3) | — |
| 4 — Fleet GC sync | — |
| 5 — Quick read | — |
| 6 — Wh meter on LCD | rbamp (the LCD driver is a separate component of your choice) |
| 7 — Multi-module (legacy sequential) | — |
| 8 — MQTT per channel | rbamp mqtt esp_wifi esp_event esp_netif nvs_flash |
| 9 — Bidirectional metering | — |
| 10 — Energy balance | — |
| 11 — Event-based logging | — |
| 12 — SPIFFS logger | rbamp spiffs |
| 13 — Deep-sleep logger | rbamp esp_sleep esp_wifi esp_event esp_netif nvs_flash mqtt (if you publish) |
The networking components also require nvs_flash_init() in app_main
and WiFi bring-up. For brevity, all of this is omitted in the snippets
below; the full project always lives in examples/<name>/.
Important note about the snippets below. Each snippet is a distillation of the key logic, not a full
app_main. In working code, the linerbamp_handle_t devis always preceded byi2c_master_bus_config_t bus_cfg = {...}; i2c_new_master_bus(&bus_cfg, &bus); rbamp_new(bus, 0x50, &dev); rbamp_begin(dev);(for single-module) orrbamp_fleet_create(bus, &fleet); rbamp_fleet_scan(fleet, true, &n);(for the fleet). When work is done —rbamp_del(dev)/rbamp_fleet_destroy(fleet)(omitted whereapp_mainenters an infinite loop — the handle lives until the chip reboots).
Scenario 1 — Mains + N sub-loads (the 80% canon): integrated metering system
Goal: the library's canonical deployment — a coherent metering system over a heterogeneous fleet, closed in a single loop:
- Discover —
rbamp_fleet_scanfinds all modules, verifies their identity, and weeds out conflicting addresses. - Configure —
rbamp_configure_channelssets the sensor class + per-channel CT models + a single terminalSAVE_USER_CONFIGin one call per multi-channel module. - GC arm —
rbamp_fleet_enable_gc_allenables General-Call latch reception on every module in the fleet. - Loop — on each tick the master performs an atomic
_gclatch(a synchronous snapshot across the whole fleet), cross-checks_check_sync, reads_poll_all, integrates energy bymaster_dt, and computes_total_power+_total_energy_wh.
This is the main example of the chapter — it shows the whole library in action on the canonical configuration. Every other scenario (2-17) breaks this flow into individual building blocks or augments it with third-party integrations (LCD, MQTT, SD).
Hardware (HW-validated): a heterogeneous fleet of 3 modules on a single bus, with an external 4.7 kΩ pull-up on SDA/SCL (see 04 · Wiring, section "Multi-module bus — the primary topology"):
@0x50— UI1, mains meter at the service entry (one current channel + voltage).@0x51— I2, a two-channel current sub-meter (no voltage).@0x52— I3, a three-channel current sub-meter (no voltage).
The reference load on the line is ~0.57 A. Lib baseline — 94c5777
(with the fix for the 3-channel configure_channels ch0 mis-bind).
Code — integrated loop
#include "rbamp.h"
#include "rbamp_fleet.h"
#include "esp_timer.h"
static const char *TAG = "metering";
/* The per-module Wh accumulator lives in the handle (the component
* already integrates by master_dt in rbamp_read_period_snapshot —
* here we only read it). */
static void configure_one(rbamp_handle_t dev) {
if (!dev) return;
uint8_t n = rbamp_channel_count(dev);
if (n == 1) { /* UI1 mains */
rbamp_set_sensor_class(dev, RBAMP_SENSOR_SCT013);
rbamp_set_ct_model(dev, /*example=*/3); /* SCT-013-030 */
rbamp_save_user_config(dev);
} else if (n == 2) { /* I2 sub-meter */
const uint8_t models[2] = { 1, 3 }; /* 5A, 30A */
rbamp_configure_channels(dev, RBAMP_SENSOR_SCT013, models, 2);
} else if (n == 3) { /* I3 sub-meter */
const uint8_t models[3] = { 1, 3, 6 }; /* 5A, 30A, 20A */
rbamp_configure_channels(dev, RBAMP_SENSOR_SCT013, models, 3);
}
}
void app_main(void) {
/* 1) bus + fleet_create — omitted for brevity */
rbamp_fleet_t fleet;
ESP_ERROR_CHECK(rbamp_fleet_create(bus, &fleet));
/* 2) discover */
size_t n_added = 0;
if (rbamp_fleet_scan(fleet, true, &n_added) == ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "bus wedged — see ch.10");
return;
}
ESP_LOGI(TAG, "fleet_scan: %u module(s), %u excluded",
(unsigned)n_added,
(unsigned)rbamp_fleet_excluded_count(fleet));
/* 3) configure per-module (mixed-CT on the sub-meters) */
for (size_t i = 0; i < rbamp_fleet_count(fleet); ++i) {
configure_one(rbamp_fleet_get(fleet, i));
}
/* 4) arm GC across the whole fleet */
size_t gc_ok = 0;
ESP_ERROR_CHECK(rbamp_fleet_enable_gc_all(fleet, /*group=*/0, &gc_ok));
ESP_LOGI(TAG, "enable_gc_all → %u/%u modules armed",
(unsigned)gc_ok, (unsigned)rbamp_fleet_count(fleet));
/* 5) loop: gclatch → check_sync → poll_all → energy + totals */
rbamp_snapshot_t snaps[RBAMP_FLEET_MAX_MODULES];
rbamp_fleet_poll_t status[RBAMP_FLEET_MAX_MODULES];
rbamp_fleet_sync_t sync_status[RBAMP_FLEET_MAX_MODULES];
uint16_t tick = 0;
while (1) {
rbamp_fleet_gclatch(fleet, /*group=*/0, tick, /*settle_ms=*/50);
size_t n_missed = SIZE_MAX;
rbamp_fleet_check_sync(fleet, tick, sync_status,
RBAMP_FLEET_MAX_MODULES, &n_missed);
size_t n_ok = 0;
rbamp_fleet_poll_all(fleet, snaps, status,
RBAMP_FLEET_MAX_MODULES, &n_ok);
/* Energy is accumulated per-handle by the component on each
* period-snapshot read. Here we read the fleet-wide aggregates. */
float p_total = 0.0f;
double e_total = 0.0;
rbamp_fleet_total_power(fleet, &p_total);
rbamp_fleet_total_energy_wh(fleet, &e_total);
size_t n_total = rbamp_fleet_count(fleet);
ESP_LOGI(TAG, "ITER %3u: GC %u/%u in_sync | poll n_ok=%u/%u | "
"P_total=%.1f W E_total=%.3f Wh",
(unsigned)tick,
(unsigned)(n_total - n_missed), (unsigned)n_total,
(unsigned)n_ok, (unsigned)n_total,
p_total, e_total);
tick++;
vTaskDelay(pdMS_TO_TICKS(1000)); /* 1 Hz for a compact summary */
}
}Bench output (HW-validated on the full Fix-A fleet, lib 94c5777 + firmware Fix-A)
INTEGRATED METERING SYSTEM — fleet_scan: 3 module(s), 0 excluded
module[0] @0x50 channels=1 voltage=yes (mains meter: voltage + 1 current)
module[1] @0x51 channels=2 voltage=no (2-channel current sub-meter)
module[2] @0x52 channels=3 voltage=no (3-channel current sub-meter)
configure_channels(2ch) → mirrors [1 2 -]
configure_channels(3ch) → mirrors [1 2 3]
enable_gc_all → 3/3 modules armed
ITER 1..N: GC in_sync=3/3 skew=0 | poll n_ok=3/3 | P_total≈1260 W | E_total: 2.49 → 4.38 Wh (accumulating)
@0x50 V=226 I=[5.58] P=[1256] (mains)
@0x51 V= — I=[11.30, 3.75] P=[—] (current sub-meter)
@0x52 V= — I=[ 3.73, 1.87, 7.50] P=[—] (current sub-meter; the 3rd input
is unused on this rig,
pickup ≈ zero)The bench figures come from uncalibrated DUTs and illustrate the shape of the data and its flow through the system — not metrological accuracy. For production accuracy, use calibrated modules.
Notes for deployment
- One synchronized loop is the whole system. Each tick: one
gclatch(a single I²C General-Call transaction) → all modules atomically start their period latch (check_sync= 3/3, skew = 0) → onepoll_allreads the whole fleet → aggregation. No drift between modules from a chain of sequential round-trips. total_powerbehavior in a heterogeneous fleet. UI1 yields active power (it has voltage → it hasP); I2/I3 are current-only, so their contribution tototal_poweris zero. Therefore, in the canon,total_power≈ the power of the mains module. For per-load power on a sub-meter, compute it on the master side:P_subload[k] ≈ V_mains × I_sub[k](an approximation, correct when the PFs are close; exact per-load P will only be available once a UI variant for sub-meters ships).- Energy (Wh) — master wall-clock.
E_totalgrows monotonically (2.55 → 6.94 Wh over the bench capture window); the component integrates each period bymaster_dt_ms(esp_timer), not by the chip-sidelatch_ms(which underreported dt by −26 % in the same session). A spurious tick adds0 Whif no new period elapsed between polls — this is normal (snap.valid = 0 on that cycle). - MISS-resilient polling. If one of the modules drops off the bus
(disconnect, reboot),
poll_allflags it withstatus[i] != ESP_OKand continues polling the rest — the master does not hang. Verified on bench: a module that disconnected mid-run left the fleet polling atn_ok=2/3without a stall. The same goes forcheck_sync—missed_idxpoints to the first out-of-sync module while the rest keep latching. - Balance analytics. In the 80% canon:
balance = P_mains − Σ(V_mains × I_sub[k])— an estimate of "the rest" (background consumption, unaccounted-for loads). Targeted code iterates over the fleet, separating the mains handle (the one withhas_voltage = true) from the sub-handles (all the others). - Per-channel currents → load disaggregation. Through
fleet_poll_allthe master sees all current channels at once (1+2+3 = 6 on our bench fleet), without separate per-module readers. - Mixed-CT on a 3-channel module requires lib ≥
94c5777. This fix closes the ch0 mis-bind bug on the 3-channel configuration — before it, mirrors [1 2 3] on I3 would not settle. On I2 (2 channels) the fix is not needed, but the bug only existed on 3-channel. - Poll latency for production sizing (measured on HW). On the bench
@50 kHz I²C: 2 modules — avg
89.4 ms(min 89.4 / max 89.6); 3 modules — avg112.1 ms(min 112.1 / max 112.3). That is ≈ 22-23 ms per additional module, linear and with very low jitter (min ≈ max). A single batchedfleet_poll_allis deterministic on a clean bus. A 3-module fleet (~112 ms) fits within a 200 ms RT window — meaning a 5 Hz polling rate is achievable without bus saturation. The full bus budget (when the fleet exceeds the 200 ms window) is in the table in 04 · Wiring, section "Bus energy budget".
Scenario 2 — Provisioning workflow (virgin → fleet)
Goal: move a new module from its factory address 0x50 to a
working address and add it to an existing fleet, persisting its
configuration (CT models, label) to flash.
Hardware (HW-validated): a virgin module on factory 0x50
(HW_VARIANT 3-channel). Provisioning moves it to address 0x52 and
saves the configuration to flash.
#include "rbamp_fleet.h"
#include "rbamp.h"
void provision_new_module(i2c_master_bus_handle_t bus,
rbamp_fleet_t fleet,
uint8_t desired_addr,
const char *label /* nullable */) {
/* PRECONDITION: exactly one virgin at 0x50 on the bus.
* MUST one-virgin-at-a-time — see ch.04 + ch.10. */
rbamp_handle_t dev = NULL;
esp_err_t err = rbamp_provision(bus, desired_addr,
/*save_config=*/true, &dev);
if (err == ESP_ERR_NOT_FOUND) {
ESP_LOGE("prov", "nobody answers at 0x50 — check VCC/I2C");
return;
}
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGE("prov", "0x50 or 0x%02X is already taken — "
"discipline violation? see ch.10",
desired_addr);
return;
}
if (err != ESP_OK) {
ESP_LOGE("prov", "provision fail: %s", esp_err_to_name(err));
return;
}
ESP_LOGI("prov", "provisioned virgin → 0x%02X (+saved)", desired_addr);
/* Label for fleet iterations (optional). */
if (label) rbamp_write_label(dev, label);
/* Bind the sensor class + CT model + one more SAVE_USER_CONFIG. */
rbamp_set_sensor_class(dev, RBAMP_SENSOR_SCT013);
rbamp_set_ct_model(dev, /*example=*/3); /* SCT-013-030 */
rbamp_save_user_config(dev);
/* Add to the fleet — from here the module is polled the usual way. */
rbamp_fleet_add(fleet, dev);
ESP_LOGI("prov", "module @0x%02X added to the fleet", desired_addr);
}Bench output (HW-validated success path, lib 94c5777):
before: present(0x50)=yes present(0x52)=no
virgin @0x50: variant=3-channel, channels=3
rbamp_provision(→0x52, save_config=true) = ESP_OK
(lib log: "provisioned virgin → 0x52 (+saved)")
post: handle now answers @0x52, is_provisioned=true
after: present(0x50)=no present(0x52)=yesis_provisioned returns true after a successful call. On a module
fresh from the factory (one nothing has touched yet) the prior
is_provisioned will be false — this is the normal 0xFB
(ERR_FLASH_PARAMS_BAD) state described in
10 · Troubleshooting, and is not fatal.
Failure paths
| Error | Condition | Recovery |
|---|---|---|
ESP_ERR_NOT_FOUND |
Nobody answers at 0x50 |
Check that the virgin module is physically connected and powered (VCC); bus-scan (10 · Troubleshooting). |
ESP_ERR_INVALID_STATE |
A conflict was detected at 0x50 or desired_addr; different modules answer identically |
Discipline violation — more than one virgin on the bus. Power-cycle + physically disconnect all but one + retry (see the recovery path in 10 · Troubleshooting). |
ESP_ERR_TIMEOUT |
The two-phase commit went through, but the module did not appear at the new address within the boot window | Power-cycle the module, retry. If it persists — flash-cycle issue. |
ESP_ERR_INVALID_ARG |
desired_addr outside 0x08..0x77 |
Use a valid 7-bit address. |
A detailed failure-mode tour: 10 · Troubleshooting,
section "Fleet: rbamp_provision returned an error".
Scenario 3 — Multi-channel mixed-CT (I3, different models per channel)
Goal: demonstrate rbamp_configure_channels on an I3 — three
channels with different CT models (e.g. ch0=SCT-013-005 for a small
load, ch1=SCT-013-030 for a medium one, ch2=SCT-013-020 for a large
one) with a single terminal flash cycle (flash-friendly), and
confirm persistence across a reboot.
#include "rbamp.h"
#include "esp_timer.h"
void configure_mixed_ct(rbamp_handle_t dev_i3) {
/* (a) Configure — three CT models in a single call. */
const uint8_t models[3] = { 1, 3, 6 };
/* 1=SCT-013-005 (5A), 3=SCT-013-030 (30A), 6=SCT-013-020 (20A) */
int64_t t0 = esp_timer_get_time();
esp_err_t err = rbamp_configure_channels(
dev_i3, RBAMP_SENSOR_SCT013, models, 3);
int64_t t1 = esp_timer_get_time();
if (err != ESP_OK) {
uint8_t reg_err;
rbamp_read_last_error(dev_i3, ®_err);
ESP_LOGE("3", "configure rejected: %s, REG_ERROR=0x%02X",
esp_err_to_name(err), reg_err);
return;
}
ESP_LOGI("3", "configure_channels latency = %lld ms",
(long long)((t1 - t0) / 1000));
/* (b) Verify the mirror via registers 0x51 / 0x52 / 0x53 (RAM). */
uint8_t m0, m1, m2;
rbamp_read_ct_model_ch(dev_i3, 0, &m0);
rbamp_read_ct_model_ch(dev_i3, 1, &m1);
rbamp_read_ct_model_ch(dev_i3, 2, &m2);
ESP_LOGI("3", "applied: [%u %u %u] (expected [1 3 6])", m0, m1, m2);
/* (c) Persist verify — reset + re-read the mirror after boot. */
rbamp_reset(dev_i3);
vTaskDelay(pdMS_TO_TICKS(300)); /* boot window */
rbamp_read_ct_model_ch(dev_i3, 0, &m0);
rbamp_read_ct_model_ch(dev_i3, 1, &m1);
rbamp_read_ct_model_ch(dev_i3, 2, &m2);
ESP_LOGI("3", "after reboot: [%u %u %u]", m0, m1, m2);
}Bench output (HW-validated on a clean-reflashed I3, lib 94c5777 +
c8ac901):
configure_channels(SCT013, {1, 3, 6}, 3) = ESP_OK
configure_channels latency = 1403 ms
applied: [1 3 6] (raw 0x01 0x03 0x06)
after reboot: [1 3 6] (persisted through CMD_RESET + boot)The "single SAVE" property — why it matters
configure_channels does one terminal CMD_SAVE_USER_CONFIG at
the end. The alternative — separate set_ct_model_ch calls for each
channel, each with its own save_user_config afterward — gives
three flash erase + write cycles.
Bench latency comparison (on the same I3):
| Approach | Latency | Flash cycles |
|---|---|---|
configure_channels(...) — 1 terminal SAVE |
~1403 ms | 1 |
3× (set_ct_model_ch + save_user_config) |
~3492 ms | 3 |
3× slower — that is (~2.5×) × ~700 ms. The key point is the
3× wear-cycle difference on the same flash page: with the typical
re-configuration of modules in the field, this turns into a meaningful
wear cost. Always use configure_channels when you need to set the
sensor class + per-channel CT models.
Valid CT model codes (Bench-characterized). On the current bench, the following are validated and working for the SCT-013 family:
1=SCT-013-005,2=SCT-013-010,3=SCT-013-030,4=SCT-013-050,6=SCT-013-020. Codes5(SCT-013-100) and7(SCT-013-060) are present in the API but not yet characterized on the current rig — attempting to use them yieldsESP_ERR_INVALID_ARG/REG_ERROR=0xFE (ERR_PARAM). In production, use only the characterized codes{1, 2, 3, 4, 6}.
Scenario 4 — Fleet GC sync (billing-grade synchronicity)
Goal: demonstrate an atomic latch across the whole fleet with a
single I²C General-Call frame. Unlike the sequential per-module
CMD_LATCH_PERIOD (where skew accumulates on every round-trip), GC
synchronizes all modules in a single bus transaction.
Hardware: the same heterogeneous UI1+I2+I3 fleet (@0x50/0x51/0x52).
#include "rbamp_fleet.h"
static const char *TAG = "gc_sync";
void app_main(void) {
/* bus init + fleet_create + fleet_scan ... as in Sc.1 */
/* Step 1: enable GC on all modules in the fleet. */
size_t gc_ok = 0;
ESP_ERROR_CHECK(rbamp_fleet_enable_gc_all(fleet, /*group=*/0, &gc_ok));
ESP_LOGI(TAG, "GC armed: %u/%u",
(unsigned)gc_ok, (unsigned)rbamp_fleet_count(fleet));
rbamp_fleet_sync_t sync_status[RBAMP_FLEET_MAX_MODULES];
uint16_t tick = 0;
for (int round = 0; round < 10; ++round) {
/* Step 2: a single broadcast latch — all modules atomically. */
ESP_ERROR_CHECK(rbamp_fleet_gclatch(fleet, /*group=*/0, tick,
/*settle_ms=*/50));
/* Step 3: in-sync check — all modules caught the same tick. */
size_t n_missed = 0;
rbamp_fleet_check_sync(fleet, tick, sync_status,
RBAMP_FLEET_MAX_MODULES, &n_missed);
size_t n_total = rbamp_fleet_count(fleet);
ESP_LOGI(TAG, "round %d tick=%u in_sync=%u/%u",
round, tick,
(unsigned)(n_total - n_missed), (unsigned)n_total);
tick++;
vTaskDelay(pdMS_TO_TICKS(100));
}
}Bench output (10 rounds on the UI1+I2+I3 fleet, 4.7 kΩ pull-up):
rbamp_fleet_enable_gc_all() → 3/3 modules armed
round 0 tick=0 in_sync=3/3 skew=0
round 1 tick=1 in_sync=3/3 skew=0
round 2 tick=2 in_sync=3/3 skew=0
round 3 tick=3 in_sync=3/3 skew=0
round 4 tick=4 in_sync=3/3 skew=0
round 5 tick=5 in_sync=3/3 skew=0
round 6 tick=6 in_sync=3/3 skew=0
round 7 tick=7 in_sync=3/3 skew=0
round 8 tick=8 in_sync=3/3 skew=0
round 9 tick=9 in_sync=3/3 skew=0When to use GC sync:
- Billing-grade synchronicity. If the per-minute/per-second
snapshot difference between modules matters for reporting (for
example, a balance calculation
mains − Σ(sub)integrated over time — module skew introduces a systematic error), GC deliversskew = 0by bench measurement. A sequential per-module latch on the same 3 modules accumulates 100-300 µs per module × N, which becomes noticeable over a 60-minute window in pinpoint analytics. - Large fleet. As the number of modules grows, the difference between "sequential N × round_trip" and "a single GC frame" grows linearly.
- Cross-deployment alignment. If you have several ESP32s on different buses sharing a master clock, the GC tick can be synchronized via the master-side wall-clock — and then snapshots from different buses are aligned within a single network round-trip window.
Witness via check_sync: a check that all modules actually
received the GC frame. It may miss a module if it was busy booting /
in a SAVE cycle at the moment of the frame — missed_idx points to
the first out-of-sync one. In bench validation, 10 rounds in a row —
0 misses.
Precondition. GC must be enabled on every module
(rbamp_fleet_enable_gc_all or per-device rbamp_enable_gc +
SAVE_USER_CONFIG + reset). A fresh module with factory defaults does
not accept GC — this is by design, a guard against accidental
broadcasts on a bring-up bus.
Scenario 5 — Quick read (single module)
Goal: print U / I / P / PF / frequency to the ESP-IDF log once a
second. The same "hello world" you wrote in
05 · Quickstart, but via rbamp_read_all() — a
single-shot read of the whole RT block into one struct.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "rbamp.h"
static const char *TAG = "quick_read";
void app_main(void) {
/* ...bus_cfg setup, rbamp_new + rbamp_begin as in the Quickstart... */
rbamp_handle_t dev = NULL;
while (1) {
rbamp_snapshot_t s;
if (rbamp_read_all(dev, &s) != ESP_OK) {
ESP_LOGE(TAG, "read fail");
vTaskDelay(pdMS_TO_TICKS(1000));
continue;
}
ESP_LOGI(TAG, "U=%.1f V f=%.1f Hz channels=%u",
(double)s.voltage, (double)s.frequency, s.channels);
for (uint8_t ch = 0; ch < s.channels; ++ch) {
ESP_LOGI(TAG, " I%u=%.2f A P%u=%.1f W PF%u=%.2f",
ch, (double)s.current[ch],
ch, (double)s.power[ch],
ch, (double)s.power_factor[ch]);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}What happens on the bus: rbamp_read_all() on a UI3 performs ~53
single-byte transactions (13 float values × 4 bytes + 1 frequency
byte). At 50 kHz with retry headroom — on the order of 25-30 ms. If you
don't need all the values, call the per-property functions
(rbamp_read_voltage(), rbamp_read_current(ch)) — fewer bytes on the
bus.
Scenario 6 — Wh meter on a 16×2 LCD
Goal: a Wh counter, refreshed once a minute, on a 16×2 character
LCD with a PCF8574 I²C expander (HD44780-compatible). The LCD sits on
the same I²C bus as the rbAmp. The component's key mechanism is
rbamp_read_period_snapshot(), which encapsulates
latch + settle + valid-check + read + Wh-tick in a single call.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "rbamp.h"
/* Minimal LCD shim — inline in the example, no third-party drivers.
* The full lcd_print/lcd_clear functions are in examples/lcd_period/main/main.c. */
void app_main(void) {
/* ...bus_cfg + rbamp + lcd init... */
rbamp_handle_t dev = NULL;
uint32_t ok_count = 0, bad_count = 0;
while (1) {
vTaskDelay(pdMS_TO_TICKS(60000));
rbamp_period_snapshot_t snap;
if (rbamp_read_period_snapshot(dev, &snap, 50, false) != ESP_OK
|| !snap.valid) {
bad_count++;
continue;
}
ok_count++;
char line1[17], line2[17];
snprintf(line1, sizeof(line1), "P=%6.1f W ", (double)snap.avg_p[0]);
snprintf(line2, sizeof(line2), "%8.4f Wh ok%lu", rbamp_energy_wh(dev, 0),
(unsigned long)ok_count);
lcd_clear();
lcd_print(0, 0, line1);
lcd_print(0, 1, line2);
}
}The full IDF project — examples/lcd_period/.
Handling stale snapshots. rbamp_read_period_snapshot() returns
ESP_ERR_INVALID_RESPONSE if the module did not manage to prepare a
new snapshot by read time. The component still records its
timestamp (esp_timer_get_time()) internally — so the next successful
snapshot is not double-counted on the interval.
Scenario 7 — Monitoring 3 modules (legacy sequential) on a single bus
Goal: poll 3 modules at addresses 0x50 / 0x51 / 0x52 from one master. Each module must have a unique address (addresses are assigned during the factory/integrator installation step — see 09 · API Reference on the address-change methods with their WARNING notes).
The canonical pattern for v1 firmware is a sequential
rbamp_latch_period()on each device + a single shared 50 ms settle + per-devicerbamp_read_period_snapshot(skip_latch=true). Inter-device skew at 100 kHz: ~1 ms per device, < 0.2 % of a 60-second period.On v1.3 firmware (
REG_VERSION = 0x04)rbamp_broadcast_latch()is fully supported once GC is enabled fleet-wide viarbamp_fleet_enable_gc_all()— the canonical fleet-sync path for billing-grade synchronization between sub-meters (see theStrategy 2fleet-GC section earlier in this chapter). On legacy v1.0-v1.2 firmware (noCAP_GC_LATCHcapability bit) the call returnsESP_ERR_NOT_SUPPORTEDand the library falls back to the per-device latch shown above. See 09 · API Reference for details.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "rbamp.h"
#define N_MODULES 3
static const char *TAG = "multi_module";
static const uint8_t ADDRS[N_MODULES] = { 0x50, 0x51, 0x52 };
void app_main(void) {
/* ...bus_cfg setup as in the Quickstart... */
i2c_master_bus_handle_t bus = NULL;
rbamp_handle_t modules[N_MODULES] = { NULL };
/* Create a handle for each module */
for (int i = 0; i < N_MODULES; ++i) {
if (rbamp_new(bus, ADDRS[i], &modules[i]) != ESP_OK ||
rbamp_begin(modules[i]) != ESP_OK) {
ESP_LOGE(TAG, "module 0x%02X init failed", ADDRS[i]);
modules[i] = NULL;
}
}
while (1) {
vTaskDelay(pdMS_TO_TICKS(60000));
/* Phase 1: sequential LATCH on each device, measure the skew */
const int64_t sync_start_us = esp_timer_get_time();
for (int i = 0; i < N_MODULES; ++i) {
if (modules[i]) rbamp_latch_period(modules[i]);
}
const uint32_t sync_us = (uint32_t)(esp_timer_get_time() - sync_start_us);
/* Phase 2: a single shared settle for all 3 modules */
vTaskDelay(pdMS_TO_TICKS(50));
/* Phase 3: read the snapshots with skip_latch=true */
ESP_LOGI(TAG, "sync_us=%u", (unsigned)sync_us);
for (int i = 0; i < N_MODULES; ++i) {
if (!modules[i]) continue;
rbamp_period_snapshot_t snap;
if (rbamp_read_period_snapshot(modules[i], &snap, 0,
/*skip_latch=*/true) != ESP_OK) {
continue;
}
ESP_LOGI(TAG, "mod%d 0x%02X P=%.0f W Wh=%.3f",
i, ADDRS[i],
(double)snap.avg_p[0],
rbamp_energy_wh(modules[i], 0));
}
}
}The full project (with bus-scan probe + error reporting) —
examples/multi_module/.
Scenario 8 — MQTT per channel
Goal: a UI3 module with 3 CT clamps on three independent lines.
Per-channel Wh counters, published to MQTT once a minute. Shows that
rbamp_energy_wh(dev, ch) works on each channel independently — no
need for a manual total_wh[3] array on the master side.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "mqtt_client.h"
#include "rbamp.h"
static const char *TAG = "ui3_mqtt";
static const char *CH_NAMES[3] = { "main", "heatpump", "lights" };
static esp_mqtt_client_handle_t mqtt;
static void publish_channel(const char *name, float avg_p, double e_wh) {
char topic[64], payload[96];
snprintf(topic, sizeof(topic), "rbamp/%s/state", name);
snprintf(payload, sizeof(payload),
"{\"power\":%.1f,\"energy\":%.4f}", (double)avg_p, e_wh);
esp_mqtt_client_publish(mqtt, topic, payload, 0, /*qos*/1, /*retain*/1);
}
void app_main(void) {
/* ...wifi STA + mqtt_client init... */
/* ...bus_cfg + rbamp_new + rbamp_begin (50 kHz via Kconfig)... */
rbamp_handle_t dev = NULL;
while (1) {
vTaskDelay(pdMS_TO_TICKS(60000));
rbamp_period_snapshot_t snap;
if (rbamp_read_period_snapshot(dev, &snap, 50, false) != ESP_OK ||
!snap.valid) {
continue;
}
for (uint8_t ch = 0; ch < rbamp_channels(dev); ++ch) {
publish_channel(CH_NAMES[ch], snap.avg_p[ch],
rbamp_energy_wh(dev, ch));
}
}
}The full project — examples/mqtt_publisher/.
For HA MQTT Auto-discovery based on this pattern — see
07 · DIY integrations +
examples/ha_discovery/.
Scenario 9 — Bidirectional metering on the master side
Goal: split signed instantaneous power into gross-consume and
gross-export. Use this pattern on the BASIC tier, where the
firmware clips negative values in snap.avg_p[ch] (period-averaged
power) — sample rbamp_read_power(dev, 0, &p) (instantaneous power,
signed on all tiers) at 5 Hz on the master side and bucket it
yourself.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "rbamp.h"
static const char *TAG = "bidir";
void bidir_task(void *arg) {
rbamp_handle_t dev = arg;
double consume_wh = 0.0, export_wh = 0.0;
int64_t t_prev_us = esp_timer_get_time();
while (1) {
vTaskDelay(pdMS_TO_TICKS(200)); /* 5 Hz, matches the RT cadence */
const int64_t t_now_us = esp_timer_get_time();
const double dt_s = (double)(t_now_us - t_prev_us) / 1e6;
t_prev_us = t_now_us;
float p;
if (rbamp_read_power(dev, 0, &p) != ESP_OK) continue;
const double dwh = (double)p * dt_s / 3600.0;
if (p >= 0.0f) consume_wh += dwh;
else export_wh += -dwh;
/* Print once a second */
static int n = 0;
if (++n % 5 == 0) {
ESP_LOGI(TAG, "P=%.1f W cons=%.4f Wh exp=%.4f Wh net=%.4f Wh",
(double)p, consume_wh, export_wh,
consume_wh - export_wh);
}
}
}This is a composite scenario with no dedicated IDF project. On the
future STANDARD / PRO firmware tiers (planned for v1.3+) the period
accumulator rbamp_energy_wh(dev, 0) will already return a signed net
balance — this master-side split is only needed for separate
gross-consume / gross-export reporting.
Scenario 10 — Whole-home energy balance
Goal: 3 modules — mains (bidirectional), solar (generation only), loads (UI3 for per-appliance). The combined dashboard is published once a minute.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "mqtt_client.h"
#include "rbamp.h"
static rbamp_handle_t mains_dev, solar_dev, loads_dev;
static esp_mqtt_client_handle_t mqtt;
void app_main(void) {
/* ...wifi STA + mqtt init... */
/* ...bus_cfg + per-module rbamp_new(bus, 0x50/0x51/0x52, ...) +
* rbamp_begin() on each... */
while (1) {
vTaskDelay(pdMS_TO_TICKS(60000));
/* Sequential LATCH + shared settle (the pattern from Scenario 3) */
rbamp_latch_period(mains_dev);
rbamp_latch_period(solar_dev);
rbamp_latch_period(loads_dev);
vTaskDelay(pdMS_TO_TICKS(50));
rbamp_period_snapshot_t sm, ss, sl;
if (rbamp_read_period_snapshot(mains_dev, &sm, 0, true) != ESP_OK || !sm.valid ||
rbamp_read_period_snapshot(solar_dev, &ss, 0, true) != ESP_OK || !ss.valid ||
rbamp_read_period_snapshot(loads_dev, &sl, 0, true) != ESP_OK || !sl.valid) {
continue;
}
char payload[256];
snprintf(payload, sizeof(payload),
"{\"mains\":%.3f,\"solar\":%.3f,"
"\"hp\":%.3f,\"ac\":%.3f,\"ev\":%.3f}",
rbamp_energy_wh(mains_dev, 0),
rbamp_energy_wh(solar_dev, 0),
rbamp_energy_wh(loads_dev, 0),
rbamp_energy_wh(loads_dev, 1),
rbamp_energy_wh(loads_dev, 2));
esp_mqtt_client_publish(mqtt, "home/energy/balance",
payload, 0, 1, 1);
}
}For an explicit gross-consume / gross-export split on the mains —
combine with the pattern from Scenario 5 (a separate 5 Hz task on
rbamp_read_power(mains_dev, 0, ...)).
This is a composite scenario with no dedicated IDF project — assemble
it from examples/mqtt_publisher/ +
examples/multi_module/.
Scenario 11 — Event-based logging (EMA)
Goal: on each 200 ms RT window, compare instantaneous power against an exponential moving average. Log significant deviations — loads such as a microwave, a kettle, or a hair dryer "appear" in the log as a turn-on / turn-off event.
#include <math.h> /* fabsf */
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "rbamp.h"
#define EMA_ALPHA 0.05f
#define EVENT_THRESHOLD_W 200.0f
static const char *TAG = "event";
void event_task(void *arg) {
rbamp_handle_t dev = arg;
float p_ema = 0.0f;
/* seed the EMA so the first reading is not a false event */
if (rbamp_read_power(dev, 0, &p_ema) != ESP_OK) p_ema = 0.0f;
while (1) {
vTaskDelay(pdMS_TO_TICKS(200));
float p;
if (rbamp_read_power(dev, 0, &p) != ESP_OK) continue;
const float delta = p - p_ema;
p_ema = (1.0f - EMA_ALPHA) * p_ema + EMA_ALPHA * p;
if (fabsf(delta) > EVENT_THRESHOLD_W) {
ESP_LOGI(TAG, "%s delta=%.0f W P=%.0f W EMA=%.0f W",
(delta > 0) ? "TURN_ON" : "TURN_OFF",
(double)delta, (double)p, (double)p_ema);
}
}
}A composite scenario — no dedicated IDF project. Combine it with the MQTT publishing from Scenario 4 for HA-side automations, or write the events to SPIFFS using the pattern from Scenario 8 for an offline log.
Scenario 12 — Local CSV logger on SPIFFS
Goal: write a period snapshot to a CSV file on SPIFFS once a minute. Useful for standalone deployments without WiFi / MQTT, or as a buffer for deferred cloud sync.
Requires a custom partition table with the entry
storage,data,spiffs,,128K,inpartitions.csv. A sample is inexamples/spiffs_logger/partitions.csv.
#include <stdio.h>
#include <sys/stat.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_spiffs.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "rbamp.h"
#define LOG_PATH "/storage/log.csv"
#define ROTATE_BYTES (64 * 1024)
static const char *TAG = "spiffs_logger";
static void rotate_if_full(void) {
struct stat st;
if (stat(LOG_PATH, &st) == 0 && st.st_size >= ROTATE_BYTES) {
rename(LOG_PATH, "/storage/log_prev.csv");
}
}
void app_main(void) {
esp_vfs_spiffs_conf_t spiffs_cfg = {
.base_path = "/storage",
.partition_label = "storage",
.max_files = 4,
.format_if_mount_failed = true,
};
ESP_ERROR_CHECK(esp_vfs_spiffs_register(&spiffs_cfg));
/* ...bus_cfg + rbamp_new + rbamp_begin... */
rbamp_handle_t dev = NULL;
while (1) {
vTaskDelay(pdMS_TO_TICKS(60000));
rbamp_period_snapshot_t snap;
if (rbamp_read_period_snapshot(dev, &snap, 50, false) != ESP_OK ||
!snap.valid) continue;
rotate_if_full();
FILE *f = fopen(LOG_PATH, "a");
if (!f) {
ESP_LOGE(TAG, "fopen failed");
continue;
}
fprintf(f, "%lld,%.3f,%.4f,%u\n",
(long long)(esp_timer_get_time() / 1000),
(double)snap.avg_p[0],
rbamp_energy_wh(dev, 0),
(unsigned)snap.master_dt_ms);
fclose(f);
}
}The full project — examples/spiffs_logger/.
The snippet above is a pedagogical simplification with a 4-column CSV
row. The example itself writes a 9-column CSV row with per-channel
avg_p and Wh for all 3 channels — more useful for production
scenarios on UI3 modules.
For deferred cloud sync (log locally, send once an hour) — see the section "Hybrid: local storage + sync" in 08 · Cloud integrations.
Scenario 13 — Battery logger with deep-sleep
Goal: an ESP32 wakes up once every 10 minutes, takes a single period latch, publishes via WiFi+MQTT, and goes back into deep-sleep. Energy persists through RTC memory between sleep cycles. The component's accumulator is disabled — the master owns the Wh persistence itself.
⚠ Important note about deep-sleep. On the current firmware,
rbamp_begin()always issues a CMD_LATCH_PERIOD primer, which resets the firmware's period accumulator. This means that after a deep-sleep wake you cannot simply callrbamp_read_period_snapshot()by default — that call would latch again and return near-zero data ~50 ms after the primer. The correct pattern below usesskip_latch=trueto read the data accumulated during the sleep interval, and the master's own RTC timer for the interval between wakeups (thesnap.master_dt_msfield reflects only the time within the current wake cycle, not wake-to-wake).
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "esp_timer.h"
#include "rbamp.h"
#define WAKE_INTERVAL_US (10ULL * 60ULL * 1000000ULL)
#define RTC_MAGIC 0xCAFEFEEDu
RTC_DATA_ATTR static uint32_t rtc_magic = 0;
RTC_DATA_ATTR static double rtc_total_wh = 0.0;
RTC_DATA_ATTR static bool rtc_primer_done = false;
RTC_DATA_ATTR static uint64_t rtc_last_wake_us = 0;
static const char *TAG = "deep_sleep";
void app_main(void) {
/* ...bus_cfg setup... */
i2c_master_bus_handle_t bus = NULL;
rbamp_handle_t dev = NULL;
if (rtc_magic != RTC_MAGIC) {
/* Cold start — initialize RTC-RAM */
rtc_magic = RTC_MAGIC;
rtc_total_wh = 0.0;
rtc_primer_done = false;
rtc_last_wake_us = 0;
}
ESP_ERROR_CHECK(rbamp_new(bus, 0x50, &dev));
ESP_ERROR_CHECK(rbamp_begin(dev)); /* issues the primer LATCH */
rbamp_energy_disable(dev); /* the master owns Wh itself */
if (!rtc_primer_done) {
/* First wake: the primer just latched the "dirty" data from
* before wake. Record the time and go straight back to sleep —
* the next wake in 10 min gets a full accumulator for the interval. */
rtc_last_wake_us = esp_timer_get_time();
rtc_primer_done = true;
esp_sleep_enable_timer_wakeup(WAKE_INTERVAL_US);
esp_deep_sleep_start();
}
/* skip_latch=true reads what the rbamp_begin() primer latched —
* that is, the accumulator for the elapsed 10-minute sleep interval. */
rbamp_period_snapshot_t snap;
esp_err_t snap_err = rbamp_read_period_snapshot(dev, &snap, /*settle_ms=*/0,
/*skip_latch=*/true);
if (snap_err != ESP_OK || !snap.valid) {
ESP_LOGW(TAG, "snapshot fail: %s (valid=%d)",
rbamp_err_to_str(snap_err), snap.valid);
esp_sleep_enable_timer_wakeup(WAKE_INTERVAL_US);
esp_deep_sleep_start();
}
/* `snap.master_dt_ms` here is the time within the current wake (after
* the primer); for energy integration we use `esp_timer_get_time()`
* against `rtc_last_wake_us`. On ESP-IDF v5.x, the value returned by
* `esp_timer_get_time()` **survives deep sleep** — the timer is
* re-derived from the RTC slow-clock on wake. So the delta gives the
* real wake-to-wake interval in microseconds. */
const uint64_t now_us = esp_timer_get_time();
const double dt_s = (double)(now_us - rtc_last_wake_us) / 1e6;
rtc_last_wake_us = now_us;
rtc_total_wh += (double)snap.avg_p[0] * dt_s / 3600.0;
ESP_LOGI(TAG, "wake: P=%.1f W Wh=%.4f dt_s=%.1f",
(double)snap.avg_p[0], rtc_total_wh, dt_s);
publish_via_wifi(snap.avg_p[0], rtc_total_wh, dt_s);
esp_sleep_enable_timer_wakeup(WAKE_INTERVAL_US);
esp_deep_sleep_start();
}The full project (with WiFi setup, MQTT publish, RTC magic-marker
guards) — examples/deep_sleep_logger/.
🛣 Roadmap. A future version of the component is expected to add
rbamp_warm_begin()— a lightweight init variant without the CMD_LATCH_PERIOD primer, which will make it simpler to readrbamp_read_period_snapshot()by default after a deep-sleep wake. Until then — the pattern above (skip_latch=true + the master's RTC timer) is canonical.
Energy budget (ESP32-WROOM, 10-minute wakeup interval):
- Active cycle duration: ~3 s (WiFi + MQTT + I²C)
- Active current: ~80 mA average → ~0.07 mAh per wake
- Sleep current: ~10 µA (RTC + retained domains)
- ~10 mAh per day → a 2000 mAh Li-ion lasts ~6 months
Scenario 14 — I3 with different CT per channel (mixed-load monitoring)
Goal: a single UI3 module monitors three independent lines on one phase with different load levels: - Channel 0 — a single outlet (SCT-013-005, ≤5 A) — TV / router / standby. - Channel 1 — the kitchen line (SCT-013-030, ≤30 A) — kettle / microwave. - Channel 2 — a water-heater boiler (SCT-013-020, ≤20 A) — peak load.
All three channels are fed from a single phase (in phase with the U channel), so P/PF/Q are correct for all three — see the single-voltage caveat in 03 · Current sensor selection.
#include "driver/i2c_master.h"
#include "rbamp.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "ui3-mixed";
void mixed_load_task(void *arg) {
rbamp_handle_t dev = (rbamp_handle_t)arg;
/* Per-channel configuration — v1.3: order is arbitrary (pure staging) */
rbamp_set_sensor_class(dev, RBAMP_SENSOR_SCT013);
rbamp_set_ct_model_ch(dev, 2, 6); /* ch2: SCT-013-020 (boiler) */
rbamp_set_ct_model_ch(dev, 1, 3); /* ch1: SCT-013-030 (kitchen) */
rbamp_set_ct_model_ch(dev, 0, 1); /* ch0: SCT-013-005 (outlet) */
while (1) {
float u, i0, i1, i2, p0, p1, p2;
rbamp_read_voltage(dev, 0, &u);
rbamp_read_current(dev, 0, &i0);
rbamp_read_current(dev, 1, &i1);
rbamp_read_current(dev, 2, &i2);
rbamp_read_power(dev, 0, &p0);
rbamp_read_power(dev, 1, &p1);
rbamp_read_power(dev, 2, &p2);
ESP_LOGI(TAG, "U=%.1f V", (double)u);
ESP_LOGI(TAG, " outlet: I=%.3f A P=%.1f W", (double)i0, (double)p0);
ESP_LOGI(TAG, " kitchen: I=%.2f A P=%.1f W", (double)i1, (double)p1);
ESP_LOGI(TAG, " boiler: I=%.2f A P=%.1f W", (double)i2, (double)p2);
ESP_LOGI(TAG, " total: P=%.1f W", (double)(p0 + p1 + p2));
vTaskDelay(pdMS_TO_TICKS(2000));
}
}When to use: monitoring a single phase with three load groups of different current magnitude. The main advantage is resolution — each clamp matches its range (an SCT-013-005 gives accurate readings on a 50 W standby load, while an SCT-013-020 does not saturate on a ~4 kW water-heater boiler).
Scenario 15 — Sub-metering: 5×UI1 on a shared bus
Goal: monitor 5 independent lines in a single-phase apartment with billing-grade synchronicity of periods. One UI1 per line (5 modules total).
This is strategy 1 for period sync from 04 · Wiring: sequential latch + shared settle.
#define N_METERS 5
static rbamp_handle_t meters[N_METERS]; /* initialized in app_main */
void sub_meter_billing_task(void *arg) {
int64_t period_start_us = esp_timer_get_time();
const int64_t PERIOD_S = 60; /* 60-second metering period */
while (1) {
/* Wait until the end of the period */
int64_t now_us = esp_timer_get_time();
int64_t elapsed_s = (now_us - period_start_us) / 1000000;
if (elapsed_s < PERIOD_S) {
vTaskDelay(pdMS_TO_TICKS((PERIOD_S - elapsed_s) * 1000));
continue;
}
/* Phase 1: sequential latch (skew ~5 ms between modules at 50 kHz) */
for (int i = 0; i < N_METERS; i++) {
rbamp_latch_period(meters[i]);
}
/* Phase 2: shared settle */
vTaskDelay(pdMS_TO_TICKS(50));
/* Phase 3: read snapshots with skip_latch=true */
float total_wh_period = 0.0f;
for (int i = 0; i < N_METERS; i++) {
rbamp_period_snapshot_t snap;
if (rbamp_read_period_snapshot(meters[i], &snap, 0, /*skip_latch=*/true) == ESP_OK
&& snap.valid) {
float wh_this_period = snap.avg_p[0] * snap.period_ms / 3600000.0f;
ESP_LOGI("meter", "[%d] avg_p=%.1f W Wh_period=%.2f",
i, (double)snap.avg_p[0], (double)wh_this_period);
total_wh_period += wh_this_period;
}
}
ESP_LOGI("meter", "TOTAL for the period: %.2f Wh\n", (double)total_wh_period);
period_start_us = esp_timer_get_time();
}
}Skew analysis: 5 modules × ~1 ms/latch = ~5 ms. Relative to the 60-s period = 0.008%. Negligible for billing.
Bus budget: 5 modules × ~5 ms RT block = 25 ms for a full round; this also fits at a 5 Hz polling rate (25 ms ≪ 200 ms). Bus headroom is comfortable up to ~15 UI1 modules.
Scenario 16 — GC broadcast latch (billing-grade sync for >8 modules)
Goal: at ≥ 8 modules the skew of strategy 1 (sequential) approaches ~10 ms. For billing-grade synchronicity — strategy 2, General-Call broadcast.
Preconfiguration (once per module):
/* Enable GC reception on each module */
uint8_t fleet_config = 0x01; /* bit0 = GC_ENABLE */
rbamp_write_reg(dev, REG_FLEET_CONFIG, &fleet_config, 1);
rbamp_save_user_config(dev); /* CMD_SAVE_USER_CONFIG, production-OK */
rbamp_reset(dev); /* CMD_RESET, settle ~300 ms */Runtime broadcast latch with witness:
#define N_METERS 12
esp_err_t billing_grade_latch(rbamp_handle_t meters[], int n,
uint16_t tick_counter) {
/* GC frame: A5 27 <group> <tick_lo> <tick_hi> on addr 0x00 */
uint8_t frame[5] = {
0xA5, /* GC_FRAME_MAGIC */
0x27, /* CMD_LATCH_PERIOD opcode */
0x00, /* group = 0x00 = all-call */
(uint8_t)(tick_counter & 0xFF),
(uint8_t)(tick_counter >> 8),
};
/* ESP-IDF: write to addr 0x00 (general-call) */
rbamp_handle_t bus_h = meters[0]; /* shared bus handle */
esp_err_t err = rbamp_bus_general_call_write(bus_h, frame, sizeof(frame));
if (err != ESP_OK) return err;
/* Settle: latch + 1 commit-cycle for witness refresh */
vTaskDelay(pdMS_TO_TICKS(50));
/* Witness check: each module should show PERIOD_VALID = 1 */
int n_ok = 0;
for (int i = 0; i < n; i++) {
uint8_t valid;
if (rbamp_read_reg(meters[i], REG_V03_PERIOD_VALID, &valid, 1) == ESP_OK
&& valid == 1) {
n_ok++;
} else {
ESP_LOGW("gc-latch", "module %d did not latch (witness=0)", i);
}
}
if (n_ok < n) {
ESP_LOGW("gc-latch", "fallback to sequential latch for %d modules",
n - n_ok);
/* Strategy 1 fallback on the remaining ones */
for (int i = 0; i < n; i++) {
uint8_t valid;
rbamp_read_reg(meters[i], REG_V03_PERIOD_VALID, &valid, 1);
if (valid != 1) {
rbamp_latch_period(meters[i]);
}
}
vTaskDelay(pdMS_TO_TICKS(50));
}
return ESP_OK;
}Skew analysis: a GC frame is ~1 ms at 50 kHz → all 12 modules latch simultaneously at the bus level. Skew is practically 0.
When to use: ≥ 8 modules and/or billing-grade accuracy.
When NOT to use: on a mixed bus with non-rbAmp devices (GC may conflict).
Scenario 17 — GC group filtering (multi-tenant billing)
Goal: 10 modules on a single bus, where you need to latch subsets independently. For example, 5 modules meter tariff A (daytime), 5 — tariff B (nighttime); they are armed and read on different schedules.
Setup (once):
/* Tariff A — modules 0..4 → group = 1 */
for (int i = 0; i < 5; i++) {
uint8_t group_id = 1;
rbamp_write_reg(meters[i], REG_GROUP_ID, &group_id, 1);
rbamp_save_user_config(meters[i]);
rbamp_reset(meters[i]);
}
/* Tariff B — modules 5..9 → group = 2 */
for (int i = 5; i < 10; i++) {
uint8_t group_id = 2;
rbamp_write_reg(meters[i], REG_GROUP_ID, &group_id, 1);
rbamp_save_user_config(meters[i]);
rbamp_reset(meters[i]);
}Runtime — independent latch per tariff:
void daytime_billing_cycle(uint16_t tick) {
/* Latch tariff A only */
uint8_t frame[5] = { 0xA5, 0x27, 0x01, (uint8_t)tick, (uint8_t)(tick >> 8) };
rbamp_bus_general_call_write(bus_h, frame, 5);
vTaskDelay(pdMS_TO_TICKS(50));
/* read the 5 modules of tariff A */
}
void nighttime_billing_cycle(uint16_t tick) {
/* Latch tariff B only */
uint8_t frame[5] = { 0xA5, 0x27, 0x02, (uint8_t)tick, (uint8_t)(tick >> 8) };
rbamp_bus_general_call_write(bus_h, frame, 5);
vTaskDelay(pdMS_TO_TICKS(50));
/* read the 5 modules of tariff B */
}
void all_modules_snapshot(uint16_t tick) {
/* group = 0x00 — all-call, all 10 latch */
uint8_t frame[5] = { 0xA5, 0x27, 0x00, (uint8_t)tick, (uint8_t)(tick >> 8) };
rbamp_bus_general_call_write(bus_h, frame, 5);
}Benefits: one cycle per tariff = clean separation of metering intervals. Without groups you would have to maintain parallel sequential latch loops with conflict resolution.
Scenario summary table
| # | LOC | I²C bus | Period? | DRDY? | MQTT? | Persistence |
|---|---|---|---|---|---|---|
| 1 | ~120 | shared 3 modules (UI1+I2+I3) | yes (GC sync) | no | yes | RAM (master-owned) |
| 2 | ~50 | dedicated (one virgin) | no | no | no | flash (save_config=true) |
| 3 | ~60 | dedicated (I3) | no | no | no | flash (SAVE_USER_CONFIG) |
| 4 | ~50 | shared 3 modules | yes (GC) | no | no | RAM |
| 5 | ~40 | dedicated | no | no | no | no |
| 6 | ~60 | shared with LCD | yes | no | no | RAM |
| 7 | ~80 | shared 3 modules | yes | no | no | RAM |
| 8 | ~70 | dedicated | yes | no | yes | retained MQTT |
| 9 | ~50 | dedicated | no (RT) | no | no | RAM (master-owned) |
| 10 | ~80 | shared 3 modules | yes | no | yes | retained MQTT |
| 11 | ~80 | dedicated | no (RT) | yes | no | RAM |
| 12 | ~70 | dedicated | yes | no | no | SPIFFS |
| 13 | ~80 | dedicated | yes | no | yes | RTC memory + MQTT |
| 14 | ~60 | dedicated I3 | no (RT) | no | no | RAM |
| 15 | ~80 | shared 5×UI1 | yes (billing) | no | no | RAM |
| 16 | ~100 | shared 12 modules | yes (GC sync) | no | no | RAM |
| 17 | ~60 | shared 10 modules multi-tenant | yes (GC groups) | no | no | RAM |
What's next
- 07 · DIY integrations — Home Assistant /
Node-RED / OpenHAB based on these scenarios (including
examples/ha_discovery/) - 08 · Cloud integrations — AWS IoT / Azure / GCP / InfluxDB Cloud
- 09 · API Reference — every public function documented
- 10 · Troubleshooting — when the scenarios behave differently on your bench