Commands
All device actions go through the COMMAND register (0x0060, u8) — on Modbus, holding 0x8113
(write-only there). A write queues the opcode; execution happens in the device's main loop
(milliseconds for most commands; flash-touching commands take longer).
Completion and result semantics
- Busy:
CMD_STATE(0x006A) bit 0 = BUSY while a command executes. BUSY is the completion signal — poll it to 0 before reading the result. A new command written while BUSY is dropped (bit 1 OVERRUN set; the running command is not clobbered; OVERRUN clears when the next command is accepted). On Modbus a COMMAND write while busy → exception 06. - Result:
ERROR(0x0002; Modbus readback holding 0x8114) holds the result after completion (0x00 = OK).ERRORreads 0xF0 (Modbus 0xFFFF) for the whole execution and is written together with completion; judge completion by BUSY, and read the result from ERROR after it. - Freshness:
CMD_SEQ(0x0067) increments once per finished command — poll it to distinguish a fresh result from a stale one. - Pattern: check BUSY = 0 → write parameters, then COMMAND → poll BUSY → 0 (timeouts: ≥1 s for
simple commands and saves, ≥0.5 s for an update page write, ≥3 s for update COMMIT; on I²C
tolerate ≥10 ms clock stretching and retry NACKed reads — never blindly resend a COMMAND write)
→ read
ERROR. Full walk-through: Sequences D2. - Commands that restart the module (0x02 RESET, 0x13 ACTIVATE, 0x16 ADDR_BY_UID on the matching module): over Modbus the reply may be truncated or absent (and a configured TX delay can suppress it); confirm by re-polling the device after the restart.
Error codes (ERROR 0x0002)
| Code | Name | Meaning |
|---|---|---|
| 0x00 | OK | accepted / completed |
| 0xF0 | PENDING | command still executing (with BUSY = 1) |
| 0xF6 | NVM_IO | a persist write failed (SAVE_CAL / SAVE_CONFIG / history import) |
| 0xF7 | NO_KEY | attestation refused — no provisioned device key |
| 0xF8 | — | operation refused by service policy |
| 0xF9 | NVM_RATE_LIMIT | persist rewrite rate-limited (min ~5 s between saves) — retry later |
| 0xFA | OP_RETRY | operation already in progress · refused in the current state (e.g. energy store FAULT, negative adopt value) |
| 0xFB | KEY_GATE | keyed command without a valid single-shot key (LT_KEY) |
| 0xFC | VALUE_CRC | update-page CRC mismatch (resend the page) · time epoch below 1 600 000 000 · UID mismatch (ADDR_BY_UID) · no such history record (HIST_READ) |
| 0xFD | PARAM | invalid parameter / config value — nothing persisted |
| 0xFE | UNKNOWN_CMD | opcode not supported |
Command set
Family commands
| Op | Name | Effect |
|---|---|---|
| 0x01 | SAVE_CAL | persist calibration registers (service/factory; UNLOCK-gated on Modbus; rate-limited → 0xF9) |
| 0x02 | RESET | warm reset (UNLOCK-gated on Modbus; expect no/truncated Modbus reply) |
| 0x03 | LATCH_PERIOD | at the next window boundary, snapshot the bank selected by LATCH_BANK (0..7; ≥ 8 → 0xFD) and restart its accumulation — capability bit 8; otherwise 0xFE |
| 0x04 | SAVE_CONFIG | persist configuration (read-modify-write; calibration untouched; not UNLOCK-gated). Values validated against the legal table; illegal → 0xFD, nothing saved; rate-limited → 0xF9; an unchanged save is an OK no-op (still raises CONFIG_CHANGED) |
| 0x06 | SET_TIME | apply the epoch written to TIME_SET/RTC_EPOCH; epoch < 1 600 000 000 → 0xFC; STATUS bit 4 sets immediately |
| 0x07 | ENERGY_RESET | family opcode, UNLOCK-gated. Not used (0xFE / exception 03) — the counter reset is the keyed LT_RESET below |
| 0x0E | IDENTIFY | blink the LEDs for 10 s — locate the module in a cabinet. Not gated |
| 0x0F | ATTEST | sign the 32-B challenge at 0x0B80 (register map); ~55 ms; 0xF7 = no key |
| 0x10 | OTA_BEGIN | open an update session (Firmware update) |
| 0x11 | OTA_BLOCK_WR | program one 256-B page (throttled to one page / ~250 ms — BUSY until its slot) |
| 0x12 | OTA_COMMIT | verify + arm the staged image (no reset); poll BUSY ≥ 3 s |
| 0x13 | OTA_ACTIVATE | reset into the armed image (only when OTA_STATUS = 2; ~4 s re-appear) |
| 0x14 | FW_ABORT | close the update session / disarm an armed image |
| 0x15 | — | reserved (factory/service) |
| 0x16 | ADDR_BY_UID | the module whose UID matches ADDR_UID takes the staged address(es), persists them together with any other pending CFG_ changes and restarts; other modules answer 0xFC. Both new-address fields zero → 0xFD; illegal address → 0xFD; save rate limit → 0xF9. Works on a Modbus broadcast. Not gated |
Keyed commands (life-time counters and history)
These change metering-relevant state, so each requires the matching single-shot key written to
LT_KEY (0x0844 / Modbus holding 0x8142–43, high word first) immediately before the command. The
key is consumed by any next command; wrong or absent key → ERROR 0xFB. This is
deliberate-action protection on top of the transport (not the Modbus UNLOCK).
| Op | Name | Key (u32) | Effect |
|---|---|---|---|
| 0x08 | HIST_READ | — (not keyed) | fetch the record selected by HIST_PERIOD/HIST_BACK into HIST_REC; no such record → 0xFC |
| 0x09 | HIST_FLUSH | — (not keyed) | flush energy now: the retention copy always; the NVM record only if ≥ 10 s since its last write (also implied by RESET and update activation) |
| 0x0A | LT_RESET | 0x4C545253 "LTRS" |
zero life-time energy; generation counter ++ (logged). Store FAULT → 0xFA |
| 0x0B | LT_ADOPT | 0x4C544150 "LTAP" |
adopt the staged counter values (board replacement / migration); generation ++ (logged). Staging order: import Wh · export Wh · reactive import varh · reactive export varh (4×i64); a negative value → 0xFA |
| 0x0C | HIST_WRITE | 0x48494D50 "HIMP" |
import the 80-B record in HIST_REC into the hourly ring (HIST_PERIOD must be 1, else 0xFC); timestamp not newer → 0xFD; NVM I/O → 0xF6 |
| 0x0D | HIST_CLEAR | 0x48434C52 "HCLR" |
step-wise erase of the hourly ring; repeat while running → 0xFA (logged) |
Life-time resets, adopts and history writes are recorded in the event log with before/after values — they cannot happen silently.
Gating summary
| Mechanism | Scope | How |
|---|---|---|
| Modbus UNLOCK | 0x01 · 0x02 · 0x07 · 0x10 · 0x12 · 0x13 · 0x14 · 0x15 | key 0xA5C3 → holding 0x8112; 10-s window, one command; may share one FC16 with the opcode (Modbus) |
| LT_KEY | 0x0A–0x0D, both transports | per-command magic, single-shot |
| Broadcast whitelist | Modbus unit 0 | only time-set, snapshot, address-by-UID (Modbus) |
Not gated on either transport: 0x04, 0x06, 0x08, 0x09, 0x0E, 0x0F, 0x11, 0x16. I²C has no UNLOCK step: physical access to the board's I²C bus is the installation's trust boundary; the keyed commands are gated on both transports regardless.