regtable.dev

Expose an MCU’s state the way a PLC exposes its registers: one register table, reachable over a serial CLI, Modbus RTU/TCP, MQTT, and a browser. Pure C99, no allocation.

The same firmware, verbatim, answers a terminal, a Modbus master, an MQTT broker, and the Web Serial panel.

State, not commands

A serial assistant is a tool for the wire: bytes in, bytes out, and the meaning stays in the operator’s head. regtable is a tool for the state behind the wire, and the wire is one of several ways in.

The difference comes from one decision: the interface is nouns, not verbs. There is no start-pump command anywhere in this library; there is a pump register, and writing 1 to it.

A command vocabulary grows with every feature, and every protocol reinvents it. A state vocabulary closes: name, type, permission, range. After that, each interface is only a wire format for the same four facts, and validation happens once, below all of them.

Modbus made the same choice in 1979; a PLC exposes registers and coils, not verbs. Two primitives then cover every interface: pull (a get, a Modbus read, a poll) and push (a value crossing its last-published shadow, an MQTT publish).

One table, six projections

Serial CLI

get · set · info · list, each with --json: one CLI for humans and machines.

Modbus RTU / TCP

FC 03/04/06/16, CRC, spec-faithful exceptions. Any SCADA or PLC master reads and writes.

MQTT

State published on change, retained; commands on <name>/set.

YAML codegen

One description generates the table and its docs; every constraint checked before compiling.

Web panel

The table renders itself in a browser: values, sliders, toggles, trends. Zero configuration.

Hooks

on_write checks, on_read computes, on_change reports.

The other half was already built

A HAL wraps thousands of silicon registers into typed handles for the firmware engineer. regtable wraps application variables into typed entries for the outside world. Together they are one chain:

silicon registers → HAL handle → application variables → RegEntry → CLI / Modbus / MQTT / panel

A register is one line

static const RegEntry registry[] = {
    { .name = "temp",     .ptr = &temp,     .type = REG_FLOAT, .perm = REG_RO },
    { .name = "interval", .ptr = &interval, .type = REG_U16,   .perm = REG_RW,
      .min.u = 100, .max.u = 60000, .modbus_addr = 1 },
    { .name = "led",      .ptr = &led,      .type = REG_BOOL,  .perm = REG_RW,
      .on_change = led_changed },
    { .name = NULL }
};  /* the table is const: it lives in flash */

C99 and the standard library, no dynamic allocation, no platform headers. The transport is two function pointers; switching chips means changing two function bodies.