ModBoss is an Elixir library that maps Modbus objects to human-friendly names and provides automatic encoding/decoding of values—making your application logic simpler and more readable, and making testing of modbus concerns easier.
Note that ModBoss doesn't handle the actual reading/writing of modbus objects—it simply assists in providing friendlier access to object values. You'll likely be wrapping another library such as Modbux for the actual reads/writes.
If available in Hex, the package can be installed
by adding modboss to your list of dependencies in mix.exs:
def deps do
[
{:modboss, "~> 0.2.0"}
]
endStarting with the type of object, you'll define the addresses to include and a friendly name for the mapping.
The :as option dictates how values will be translated. Passing an atom will
cause ModBoss to search for corresponding encode_/decode_ functions within
the same module as your schema. Alternatively, you can use encoder functions
from another module—such as those provided out of the box by ModBoss.Encoding.
See ModBoss.Schema for details.
Decode functions will receive the values to decode and must return
{:ok, decoded} or {:error, message}.
defmodule MyDevice.Schema do
use ModBoss.Schema
schema do
holding_register 1, :outdoor_temp, as: {ModBoss.Encoding, :signed_int}
holding_register 2..5, :model_name, as: {ModBoss.Encoding, :ascii}
holding_register 6, :version, as: :fw_version, mode: :rw
end
def encode_fw_version(value) do
encoded_value = do_encode(value)
{:ok, encoded_value}
end
def decode_fw_version(value) do
decoded_value = some_decode_logic(value)
{:ok, decoded_value}
end
endIn this example:
- Holding register at address 1 is named
outdoor_tempand uses the built-insigned_intdecoder that ships with ModBoss. - Holding registers 2–5 are grouped under the name
model_nameand use a built-in ASCII decoder. - Holding register 6 is named
versionand usesencode_fw_version/2anddecode_fw_version/1to translate values being written or read respectively.
You'll need to provide a read_func/3 and a write_func/3 for actually
interacting on the Modbus. In practice, these functions will likely build on a library like
Modbux along with state stored in a GenServer (e.g.
a modbux_pid, IP Address, etc.) to perform the read/write operations.
For each batch, the read_func will be provided the object type
(:holding_register, :input_register, :coil, or :discrete_input), the starting address,
and the number of addresses to read. It must return either {:ok, result} or {:error, message}.
read_func = fn object_type, starting_address, count ->
result = custom_read_logic(…)
{:ok, result}
endFor each batch, the write_func will be provided the type of object (:holding_register or
:coil), the starting address for the batch to be written, and a single value or list of values
to write. It must return either :ok or {:error, message}.
write_func = fn object_type, starting_address, value_or_values ->
custom_write_logic(…)
:ok
endFrom here you can read and write by name…
iex> ModBoss.read(MyDevice.Schema, :outdoor_temp, read_func)
{:ok, 72}iex> ModBoss.read(MyDevice.Schema, [:outdoor_temp, :model_name, :version], read_func)
{:ok, %{outdoor_temp: 72, model_name: "AI4000", version: "0.1"}}iex> ModBoss.write(MyDevice.Schema, %{version: "0.2"}, write_func)
:okExtracting your Modbus schema allows you to isolate the encode/decode logic
making it much more testable. Your primary application logic becomes simpler and more
readable since it references mappings by name and doesn't need to worry about encoding/decoding
of values. It also becomes fairly straightforward to set up virtual devices with the exact
same object mappings as your physical devices (e.g. using an Elixir Agent to hold the state of
the modbus objects in a map). And it makes for easier troubleshooting since you don't need to
memorize (or look up) the object mappings when you're at an iex prompt.
ModBoss automatically batches contiguous addresses into a single Modbus request.
The :max_gap option on ModBoss.read/4 lets you go further by bridging small
gaps between requested mappings—trading a few extra (discarded) addresses for
fewer network round trips. See ModBoss.read/4 for details, and the :gap_safe
option in ModBoss.Schema for controlling which mappings are safe to read
incidentally.
ModBoss optionally emits :telemetry events for
reads and writes. To enable telemetry, add :telemetry to your dependencies:
def deps do
[
# …other deps
{:telemetry, "~> 1.0"}
]
endSee ModBoss.Telemetry for the full list of events, measurements, and metadata.