Skip to content

Built-in fields

A field is a descriptor you place on a Component. It owns the codec (how raw register words become a Python value) but holds no per-read state. Reading the attribute returns T | None — the decoded value, or None before the first read or when a sentinel decodes to “no value”.

Prefer the factories below over constructing field classes directly; they are named presets (width, sign, sentinel, scale) over a small set of codecs. Every factory lives in modbus_connection.model:

from modbus_connection.model import (
integer, gauge, raw_register,
uint32, int32, uint64, int64,
float32, float64,
string, enum, flags,
coil, discrete_input,
)

Most register factories accept these keyword arguments. Not every option applies to every factory (a string has no scale, for instance) — the tables per factory below list what each one takes.

Option Meaning
address Address of the value’s first register word (before stride/base_offset).
count Number of 16-bit registers the value spans (fixed by most factories).
scale Affine multiplier: the value decodes as raw * scale + offset.
offset Affine addend, for a device that reports a shifted value.
nan A raw sentinel value that decodes to None (device “unimplemented”).
signed Interpret the raw integer as two’s-complement.
word_order "big" (default, ABCD) or "little" (CDAB) for multi-register values.
unit Unit-of-measure label carried as metadata; not used in decoding.
stride Per-index address step for a repeated sub-unit.
writable True, or a validator callable.
force_fc16 Always write with FC16, even a single register. Requires writable.
scale_register Address of a SunSpec scale-factor register applied as 10**sf.
scale_register_stride Per-index step for scale_register.

Numeric fields decode as raw * scale + offset. Pass offset for a device that reports a shifted value — e.g. gauge(0, 0.1, offset=-100) for a temperature stored as raw * 0.1 - 100. Writable fields invert it as (value - offset) / scale.

Many devices send a reserved value to mean “this point is not implemented”. Pass nan= that raw value and the field decodes it to None:

temperature = gauge(5, 0.1, nan=0x8000) # 0x8000 -> None

word_order selects the order of the 16-bit registers in a multi-register value. It defaults to "big" (the Modbus convention), covering the ABCD arrangement; pass "little" for CDAB.


An unscaled integer register — counts, percentages, addresses.

integer(address, *, offset=0.0, signed=True, nan=None, stride=0,
writable=False, scale_register=None, scale_register_stride=0,
unit=None, force_fc16=False) -> NumberField[int]
count = integer(4) # signed 16-bit int
percent = integer(7, signed=False) # 0..65535
shifted = integer(2, offset=-100) # raw - 100

A scaled numeric register — a 0.1-scaled temperature, a voltage, and so on. The one factory where scale is a required positional argument.

gauge(address, scale, *, offset=0.0, signed=True, nan=None, stride=0,
writable=False, scale_register=None, scale_register_stride=0,
unit=None, force_fc16=False) -> NumberField[float]
voltage = gauge(0, 0.1, unit="V") # raw * 0.1
temp = gauge(9, 0.1, offset=-40, unit="°C") # raw * 0.1 - 40

A single raw register word — no scaling, sign handling, or sentinel. Useful for a status word you decode yourself.

raw_register(address, *, stride=0, writable=False, force_fc16=False) -> RawField

uint32 / int32 span two consecutive registers; uint64 / int64 span four. All take scale, offset, word_order, unit, and the write options.

uint32(address, *, scale=1.0, offset=0.0, word_order="big", stride=0,
writable=False, unit=None, force_fc16=False) -> NumberField[int]
# int32 / uint64 / int64 have the same signature.
energy = uint32(2, unit="Wh") # 32-bit over registers 2–3
signed_power = int32(10, word_order="little") # CDAB word order
lifetime = uint64(20, unit="Wh") # 64-bit over registers 20–23

float32 decodes an IEEE-754 single over two registers; float64 a double over four.

float32(address, *, scale=1.0, offset=0.0, word_order="big", stride=0,
writable=False, unit=None, force_fc16=False) -> FloatField
# float64 is identical but spans four registers.
flow = float32(40, unit="m³/h")
precise = float64(50)

string reads a fixed-length null-padded ASCII string over length registers (two characters per register).

string(address, length, *, stride=0, writable=False, force_fc16=False) -> StringField
serial = string(100, 8) # 8 registers -> up to 16 ASCII characters

Map a raw register natively to an IntEnum or IntFlag.

  • enum — an IntEnum field. A code with no member decodes to None (warned once per distinct value).
  • flags — an IntFlag field. Unknown bits are kept.
enum(address, enum_type, *, count=1, signed=False, word_order="big",
nan=None, stride=0, writable=False, force_fc16=False) -> NumberField[E]
flags(address, flag_type, *, count=1, signed=False, word_order="big",
nan=None, stride=0, writable=False, force_fc16=False) -> NumberField[F]
from enum import IntEnum, IntFlag
class Mode(IntEnum):
OFF = 0
HEAT = 1
COOL = 2
class Alarms(IntFlag):
OVERTEMP = 1
UNDERVOLT = 2
class Device(Component):
mode = enum(3, Mode)
alarms = flags(4, Alarms)

signed interprets the code as two’s-complement for devices with negative enum codes (e.g. -1 sent as 0xFFFF); the default is unsigned.

Single-bit fields decode to bool | None. Each carries its own space, so a component may mix them freely.

  • coil — a coil (FC01). Read/write; pass writable=True to allow writes.
  • discrete_input — a discrete input (FC02). Read-only — it has no writable option because discrete inputs are physically read-only.
coil(address, *, writable=False, stride=0) -> CoilField
discrete_input(address, *, stride=0) -> DiscreteInputField
class IO(Component):
relay = coil(0, writable=True)
fault = discrete_input(0) # distinct address space from coil 0

writable=True marks a field writable and writes the value as-is. Passing a validator callable instead both marks it writable and vets the value before each write — it receives the requested value and returns the value to actually write, or raises to reject it, before anything reaches the device:

def in_range(value: int) -> int:
if not 0 <= value <= 100:
raise ValueError(f"{value} out of range")
return value
class Boiler(Component):
setpoint = integer(0, writable=in_range)

For registers, write() picks FC06 for a single word and FC16 for multiple. Pass force_fc16=True for a device that honours only FC16 even for one register.

scale_register points at a separate register whose signed int16 value is read alongside the field and applied as 10**sf — the SunSpec sunssf convention. Writing a dynamically-scaled field is unsupported (it raises). See the SunSpec page for the pre-wired point types built on this.

The factories return instances of a small set of codec classes — NumberField, FloatField, StringField, RawField, and the address types (IPv4Field, IPv6Field, Eui48Field) — plus CoilField / DiscreteInputField for bits. Reach for a subclass directly only for a codec the factories don’t cover; almost everything is expressible with the factories above.