Sequences — step-by-step control flows
Canonical host-side flows. Every step names the register (I²C address; Modbus in brackets) and what to check. The command-polling rules used everywhere here are defined once in D2.
D1 — Power-up / discovery
- Read 12 bytes at 0x0000 in one transaction:
ID= 'S','T' ·VERSION≥ 0x36 ·CAPABILITIES(expect bit 9 set; THD bit 2 = 0, snapshot-bank bit 8 = 1 when banks are present, on-device energy bit 13 = 1) ·STATUS·N_VOLTAGE/N_CURRENT·VARIANT(UI1 0x11 = 1/1, 0x63 = 3/3) ·HW_REV/FW_VERSION. - Read
UID0x00F4 (12 B) — the device identity for your database;FW_VER0x0114 (u32 image version);BOARD_ID0x017C. - Wait for live data:
WIN_SEQ(0x0016) ≠ 0 and stepping,STATUSbit 1 (sensor_ok) = 1 — allow ~1 s after a boot (boot readiness). - If
STATUSbit 4 (time_valid) = 0 → set the clock (D2, SET_TIME). - Health gate: cal_ok or
CAL_SRC(0x0872) ∈ {1, 2};LTA_STATUS(0x0870) ≤ 1. - Re-arm RAM state:
EVENT_MASK(0x00C1),THRESH_I/P(0x00C4/0x00C8); acknowledge the boot by clearing RESET_OCCURRED — write 0x20 toEVENT_FLAGS(0x00C0).
Modbus: the same identity set is input 0x6000–0x601F (mirror);
FW_VER holding 0x8315–16, BOARD_ID holding 0x831B–1C.
D2 — Executing a command
- Idle check:
CMD_STATE(0x006A; holding 0x8116) bit 0 = 0. - Write the command's parameter registers, then the opcode to
COMMAND(0x0060; holding 0x8113 — for a Modbus gated opcode send one FC16{0x8112 = 0xA5C3, 0x8113 = opcode}; exception 06 = busy, exception 03 = unknown opcode or missing unlock). - Poll
CMD_STATEbit 0 → 0. Timeouts: ≥ 1 s simple commands and saves · ≥ 0.5 s update page write · ≥ 3 s update COMMIT. On I²C tolerate ≥ 10 ms clock stretching; retry NACKed reads; never blindly resend a COMMAND write (a duplicate would be dropped as OVERRUN, but you lose track of which write took). - Read
ERROR(0x0002; holding 0x8114) after BUSY = 0: 0x00 = OK, codes in Commands. Optionally verifyCMD_SEQ(0x0067) stepped by 1 and OVERRUN (bit 1) = 0.
Time set (the most common command): write epoch u32 LE to TIME_SET 0x08C0 (Modbus FC16
0x8100 = high word, 0x8101 = low word — on broadcast this is its own frame, the command follows as a
second frame) → COMMAND 0x06 → poll → ERROR 0xFC = epoch below 1 600 000 000. STATUS bit 4
sets immediately.
D3 — Configuration
- Write the wanted
CFG_*registers (0x0070–0x0085; holding 0x8000–0x800C). COMMAND0x04 SAVE_CONFIG (not UNLOCK-gated) → D2.ERROR0xFD = illegal value, nothing saved · 0xF9 = rate limit, retry after ~5 s · 0xF6 = store I/O error.- If the change needs a restart (I²C address, Modbus unit/baud/frame/tx-delay,
CFG_VIRTUAL_IDS):COMMAND0x02 RESET (UNLOCK-gated on Modbus; expect no/truncated Modbus reply) and re-poll the device after ~1 s.
CFG_LED / CFG_WORD_ORDER act on write but revert at boot unless saved. CFG_CLASS,
CFG_CT_MODEL, CFG_TZ_OFFSET_MIN, CFG_TIME_AUTH, CFG_ENERGY_PERSIST_S are stored metadata.
D4 — Address / serial change
I²C address (known device): CFG_I2C_ADDR 0x0073 = 0x08..0x77 → SAVE_CONFIG → BUSY 0 and
ERROR 0 → RESET → wait ~1 s → probe ID = 'S','T' at the new address.
Modbus serial: holding 0x8000 unit (1–247) / 0x8001 baud (0–10) / 0x8002 frame / 0x8003 TX delay
(validated on write — exception 03) → FC06 0x8113 = 0x0004 → poll 0x8114 = 0 → FC16
{0x8112 = 0xA5C3, 0x8113 = 0x0002} (reply may be lost) → reconnect at the new settings.
By UID (several modules on one address, both transports):
1. I²C: one write of 0x00E0: UID (12 B) + new I²C address (0x00EC, or 0) + new Modbus unit (0x00ED,
or 0). Modbus: FC16 0x8118–0x811E (UID byte pairs + 0x811E = addr<<8 | unit) — broadcast allowed.
2. COMMAND 0x16 (FC06 0x8113 = 0x0016; broadcast allowed).
3. The matching module persists (together with any staged CFG_ values) and restarts; every other
module answers ERROR 0xFC. Both new-address fields 0 or an illegal address → 0xFD; save rate
limit → 0xF9.
4. Probe the module at its new address/unit; identify by UID.
D5 — Energy: read, reset, adopt
Read (each interval boundary, your clock):
1. One I²C transaction ≤ 128 B over 0x0800–0x0843: Wh counters, mWh counters, LT_GEN
(coherent snapshot). Plus UPTIME 0x08A0. Modbus: input 0x6B00/0x6B14 + holding 0x8140.
Capability bit 15: the same counters at µWh resolution — grand at 0x08D0
(3×i64; Modbus input 0x6BB0), per channel at 0x0C80 + g·0x10 (Modbus 0x6BC0 + g·8) — same
snapshot as the mWh fields (imp/exp mWh = floor(µWh/1000); net = imp − exp in the same unit),
same delta/generation/restart rules. The module also publishes grand reactive µvarh at 0x08E8/0x08F0
(Modbus 0x6BAC/0x6BBC, not contiguous): the coherent fine-resolution read is one transaction
over 0x08D0–0x08F7 (40 B).
2. Interval energy = delta of the mWh counters (µWh counters where finer resolution is wanted).
3. LT_GEN changed → do not take the delta; re-anchor (someone reset/adopted — see the event log).
4. UPTIME decreased → module restarted: delta = max(0, Δ) (up to about the last second of
energy may be missing), re-feed time (D2).
Reset (deliberate, logged): LT_KEY 0x0844 = 0x4C545253 ("LTRS"; Modbus FC16 0x8142–43, high
word first) → immediately COMMAND 0x0A (any command consumes the key) →
D2. ERROR 0xFB = key wrong/missing · 0xFA = store FAULT. No Modbus
UNLOCK needed — the key is the gate. (The family opcode 0x07 ENERGY_RESET is not used:
0xFE / exception 03.)
Adopt (board swap / migration): stage 4×i64 into LT_ADOPT 0x0848 (Modbus 0x8144–0x8153) in the
order import Wh · export Wh · reactive import varh · reactive export varh → LT_KEY = 0x4C544150
("LTAP") → COMMAND 0x0B. Negative value → 0xFA. Both reset and adopt bump LT_GEN and land in the
event log with before/after values.
D6 — History and the event log
HIST_PERIOD0x0C00 (holding 0x8160): 0 = last 15-min (thenHIST_BACKmust be 0) · 1 = hourly ring · 2 = event log.HIST_BACK0x0C04 (holding 0x8163) = n, 0 = newest.COMMAND0x08 HIST_READ → D2.ERROR0xFC = no such record.- On success:
HIST_FLAGS0x0C02 bit 0 = 1; readHIST_COUNT/HIST_RECSZ(80 history / 32 event) /HIST_OLDEST/HIST_NEWESTto map timestamps to indices. - Read the record at 0x0C20 (
HIST_RECSZbytes, one transaction; Modbus holding 0x8180+ byte pairs). Layouts: Measurement & energy. - Iterate
HIST_BACK= 0 … COUNT−1 to read the records; diff rules per the record FLAGS.
D7 — Firmware update
FW_ACTIVE_BANK0x0156 (holding 0x8301) → pick the release file for the other bank.- Gate:
BOARD_ID0x017C == the file's board word; file version ≥FW_VER0x0114; readOTA_BANK_SIZE0x014C (never hardcode it). - (Modbus: UNLOCK)
COMMAND0x10 BEGIN → D2 →OTA_STATUS0x0140 (holding 0x8300) = 1. - Per non-blank 256-B page, ascending, page 0 first:
OTA_IDX0x0144 = k · payload → 0x0500 (Modbus: two FC16 of 64 regs at 0x8380/0x83C0) · optionalOTA_PCRC_EXP0x0170 = page CRC-32 ·COMMAND0x11 (not gated) · poll BUSY (≤ ~0.5 s) ·ERROR0xFC → rewrite payload, repeat page · checkOTA_STATUSstill 1 (3 → readOTA_DIAG0x0180, restart from BEGIN). - (UNLOCK)
COMMAND0x12 COMMIT → poll BUSY ≥ 3 s →OTA_STATUS: 2 = armed (FW_STAGED_VERSION0x0150 = new version) · 3 = refused (reasons 7–15 inOTA_DIAG) · 4 = lower version. - Only when status = 2: (UNLOCK)
COMMAND0x13 ACTIVATE — no/truncated Modbus reply; the module is back in ~4 s. Or (UNLOCK)COMMAND0x14 FW_ABORT to drop the armed image. - Do not reset the module for ~10 s (trial + self-confirm), then verify:
FW_ACTIVE_BANKflipped ·FW_VER= new version ·SWAP_PENDING0x011C = 0. OldFW_VER= it rolled back — readOTA_DIAG.
D8 — Modbus unlock
FC06 holding 0x8112 = 0xA5C3 → valid 10 s, consumed by one gated command; a wrong value clears an active unlock; missing/expired → exception 03 on the gated command. Key and opcode may share one FC16 at 0x8112–0x8113.
Gated: 0x01 · 0x02 · 0x07 · 0x10 · 0x12 · 0x13 · 0x14 · 0x15. Not gated: 0x04 · 0x06 · 0x08–0x0F · 0x11 · 0x16.