ModBoss

Show that Bus who's Boss!
ModBoss is an Elixir library that maps Modbus registers 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 registers—it simply assists in providing friendlier access to register values. You'll likely be wrapping another library such as Modbux for the actual reads/writes.
Installation
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.1.0"}
]
end
Usage
1. Map your schema
Starting with the type of register, you'll define the addresses to include and a friendly name for the mapping.
The :as
option dictates how values will be encoded before being written to Modbus or decoded
after being read from Modbus. You can use translation functions from another module—like those
found in ModBoss.Encoding
—or provide your own as shown here with as: :fw_version
.
When providing your own translation functions, ModBoss expects that you'll provide functions
corresponding to the :as
option but with encode_
/ decode_
prefixes added as applicable.
These functions will receive the value to be translated and should return either
{:ok, translated_value}
or {:error, message}
.
defmodule MyDevice.Schema do
use ModBoss.Schema
modbus_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
# Also supports: input_register / coil / discrete_input
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
end
In this example:
- Holding register at address 1 is named
outdoor_temp
and uses the built-insigned_int
decoder that ships with ModBoss. - Holding registers 2–5 are grouped under the name
model_name
and use a built-in ASCII decoder. - Holding register 6 is named
version
and usesencode_fw_version/1
anddecode_fw_version/1
to translate values being written or read respectively.
2. Provide generic read/write functions
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 type of register
(: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 register_type, starting_address, count ->
result = custom_read_logic(…)
{:ok, result}
end
For each batch, the write_func
will be provided the type of register (: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}
.
write_func = fn register_type, starting_address, value_or_values ->
result = custom_write_logic(…)
{:ok, result}
end
3. Read & Write by name!
From here you can read and write by name…
Requesting a single value returns just one value:
iex> ModBoss.read(MyDevice.Schema, read_func, :outdoor_temp)
{:ok, 72}
Requesting multiple values returns a map:
iex> ModBoss.read(MyDevice.Schema, read_func, [:outdoor_temp, :model_name, :version])
{:ok, %{outdoor_temp: 72, model_name: "AI4000", version: "0.1"}}
Writing is performed via a keyword list or map:
iex> ModBoss.write(MyDevice.Schema, write_func, version: "0.2")
:ok
Benefits
Extracting 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 registers 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 register mappings as your physical devices (e.g. using an Elixir Agent to hold the state of
the registers in a map). And it makes for easier troubleshooting since you don't need to
memorize (or look up) the register mappings when you're at an iex
prompt.