Human-friendly modbus reading, writing, and translation.
Read and write modbus values by name, with automatic encoding and decoding.
Telemetry
ModBoss optionally emits telemetry events for reads and writes via the
:telemetry library. See
ModBoss.Telemetry for all event names, measurements, and metadata.
Summary
Functions
Encode values per the mapping without actually writing them.
Read from modbus using named mappings.
Write to modbus using named mappings.
Types
@type read_func() :: (ModBoss.Mapping.object_type(), starting_address :: ModBoss.Mapping.address(), count :: ModBoss.Mapping.count() -> {:ok, any()} | {:error, any()})
@type write_func() :: (ModBoss.Mapping.object_type(), starting_address :: ModBoss.Mapping.address(), value_or_values :: any() -> :ok | {:error, any()})
Functions
Encode values per the mapping without actually writing them.
This can be useful in test scenarios and enables values to be encoded in bulk without actually being written via Modbus.
Returns a map with keys of the form {type, address} and encoded_value as values.
Opts
:context— a map of arbitrary data that will be included in theModBoss.Encoding.Metadatastruct passed to encode functions (when using 2-arity encoders). Defaults to%{}.
Example
iex> ModBoss.encode(MyDevice.Schema, foo: "Yay")
{:ok, %{{:holding_register, 15} => 22881, {:holding_register, 16} => 30976}}
Read from modbus using named mappings.
This function takes either an atom or a list of atoms representing the mappings to read, batches the mappings into contiguous addresses per type, then reads and decodes the values before returning them.
For each batch, read_func will be called with the type of modbus object (:holding_register,
:input_register, :coil, or :discrete_input), the starting address for the batch
to be read, and the count of addresses to read from. It must return either {:ok, result}
or {:error, message}.
If a single name is requested, the result will be an :ok tuple including the single result for that named mapping. If a list of names is requested, the result will be an :ok tuple including a map with mapping names as keys and mapping values as results.
Opts
:max_gap— Allows reading across unrequested gaps to reduce the overall number of requests that need to be made. Results from gaps will be retrieved and discarded. This value can be specified as a single integer (which applies to all types) or per object type. Only readable ModBoss mappings will be included in gap reads. To opt a readable mapping out of gap reads, specifygap_safe: falseon that mapping. See the:gap_safeoption inModBoss.Schemafor details.:debug— iftrue, returns a map of mapping details instead of just the value; defaults tofalse:decode— iffalse, ModBoss doesn't attempt to decode the retrieved values; defaults totrue. This option can be especially useful if you need insight into a particular value that is failing to decode as expected.:max_attempts— maximum number of times eachread_funccallback will be attempted before giving up. Defaults to1(no retries). Only{:error, _}triggers a retry; exceptions are not retried.:context— a map of arbitrary data that will be included in theModBoss.Encoding.Metadatastruct passed to decode functions (when using 2-arity decoders) and in telemetry event metadata. Defaults to%{}. Useful for conditionally decoding values based on runtime information like firmware version or hardware revision, and for identifying which device or connection a request belongs to in telemetry handlers.
Gaps
Every Modbus request incurs network round-trip overhead, so fewer, larger reads are often faster than many small ones—even if some of the addresses in between aren't needed.
The :max_gap option allows you to specify how many unrequested addresses
you're willing to include in a single read in order to bridge the gap between
requested mappings and reduce the total number of requests.
If enabled, a single request may bridge multiple gaps, each up to that size.
A gap will only be bridged if every address within it belongs to a
known, gap-safe mapping (gap_safe: true, the default for readable mappings).
Unmapped addresses and mappings with gap_safe: false both prevent a gap
from being bridged.
For example, given this schema:
schema do
holding_register 1, :temp, as: {ModBoss.Encoding, :signed_int}
holding_register 2, :status, as: {ModBoss.Encoding, :unsigned_int}
holding_register 3, :error_count, as: {ModBoss.Encoding, :unsigned_int}, gap_safe: false
holding_register 4, :mode, as: {ModBoss.Encoding, :unsigned_int}
holding_register 5, :humidity, as: {ModBoss.Encoding, :unsigned_int}
endReading :temp and :humidity with max_gap: 5 will not batch them
into a single request because address 3 (:error_count) is not gap-safe.
Removing :error_count from the schema wouldn't help either—the address
would then be unmapped, which also prevents bridging.
Examples
read_func = fn object_type, starting_address, count ->
result = custom_read_logic(…)
{:ok, result}
end
# Read one mapping
ModBoss.read(SchemaModule, :foo, read_func)
{:ok, 75}
# Read multiple mappings
ModBoss.read(SchemaModule, [:foo, :bar, :baz], read_func)
{:ok, %{foo: 75, bar: "ABC", baz: true}}
# Read *all* readable mappings
ModBoss.read(SchemaModule, :all, read_func)
{:ok, %{foo: 75, bar: "ABC", baz: true, qux: 1024}}
# Enable reading extra registers to reduce the number of requests
ModBoss.read(SchemaModule, [:foo, :bar], read_func, max_gap: 10)
{:ok, %{foo: 75, bar: "ABC"}}
# …or allow reading across different gap sizes per type
ModBoss.read(SchemaModule, [:foo, :bar], read_func, max_gap: %{holding_register: 10})
{:ok, %{foo: 75, bar: "ABC"}}
# Get "raw" Modbus values (as returned by `read_func`)
ModBoss.read(SchemaModule, [:foo, :bar], read_func, decode: false)
{:ok, %{foo: [75], bar: [16706, 17152]}}
@spec write(module(), values(), write_func(), keyword()) :: :ok | {:error, any()}
Write to modbus using named mappings.
ModBoss automatically encodes your values, then batches any encoded values destined for
contiguous objects—creating separate batches per object type.
For each batch, write_func will be called with the type of object (:holding_register or
:coil), the starting address for the batch to be written, and a list of values to write.
It must return either :ok or {:error, message}.
Batch values
Each batch will contain either a list or an individual value based on the number of addresses to be written—so you should be prepared for both.
Non-atomic writes!
While ModBoss.write/4 has the feel of being atomic, it's important to recognize that it
is not! It's fully possible that a write might fail after prior writes within the same call to
ModBoss.write/4 have already succeeded.
Within ModBoss.write/4, if any call to write_func returns an error tuple,
the function will immediately abort, and any subsequent writes will be skipped.
Opts
:max_attempts— maximum number of times eachwrite_funccallback will be attempted before giving up. Defaults to1(no retries). Only{:error, _}triggers a retry; exceptions are not retried.:context— a map of arbitrary data that will be included in theModBoss.Encoding.Metadatastruct passed to encode functions (when using 2-arity encoders) and in telemetry event metadata. Defaults to%{}. Useful for conditionally encoding values based on runtime information like firmware version or hardware revision, and for identifying which device or connection a request belongs to in telemetry handlers.
Example
write_func = fn object_type, starting_address, value_or_values ->
custom_write_logic(…)
:ok
end
iex> ModBoss.write(MyDevice.Schema, [foo: 75, bar: "ABC"], write_func)
:ok