Modbuzz is a MODBUS library with a small public API for TCP, RTU, and gateway use cases.
The Modbuzz module is the external API entrypoint:
- Request data (
request/2,request/3,request/4,request_async/2,request_async/3,request_async/4,request_async/5) - Manage optional data-server content (
create_unit/1,create_unit/2,upsert/3,upsert/4,delete/2,delete/3,dump/1,dump/2) - Start and stop TCP/RTU clients and servers
Summary
Functions
Create a unit under a data server.
Delete a request mapping from a data server unit.
Dump all request mappings from a data server unit.
Send a synchronous request and wait for the result.
Send an asynchronous request and return immediately.
Start a data server instance.
Start an RTU client instance.
Start an RTU server instance.
Start a TCP client instance.
Start a TCP server instance.
Stop a data server instance.
Stop an RTU client instance.
Stop an RTU server instance.
Stop a TCP client instance.
Stop a TCP server instance.
Upsert a request response/callback pair into a data server unit.
Types
@type client() :: GenServer.name()
@type data_server() :: GenServer.name()
@type error() :: Modbuzz.PDU.Protocol.t()
@type request() :: Modbuzz.PDU.Protocol.t()
@type response() :: Modbuzz.PDU.Protocol.t()
@type server() :: GenServer.name()
@type unit_id() :: 0..247
Functions
@spec create_unit(name :: data_server(), unit_id()) :: :ok | {:error, :already_created}
Create a unit under a data server.
Returns
:okwhen unit is created.{:error, :already_created}when the unit already exists.
@spec delete(name :: data_server(), unit_id(), request()) :: :ok | {:error, :unit_not_found}
Delete a request mapping from a data server unit.
Returns
:okwhen mapping is deleted.{:error, :unit_not_found}when target unit does not exist.
@spec dump(name :: data_server(), unit_id()) :: {:ok, map()} | {:error, :unit_not_found}
Dump all request mappings from a data server unit.
Returns
{:ok, map}when unit exists.{:error, :unit_not_found}when target unit does not exist.
@spec request( name :: client() | data_server(), unit_id(), request(), non_neg_integer() ) :: {:ok, response()} | {:error, error()} | {:error, reason :: term()}
Send a synchronous request and wait for the result.
Use this function when your flow is simple and blocking behavior is acceptable.
Returns
{:ok, response}on normal response.{:error, error_response}on MODBUS exception response.{:error, reason}on local/network/runtime failures.
Common failures
{:error, :timeout}when no response arrives beforetimeout.- Caller exits with
:noprocif target process is not running (GenServer.call/3behavior).
@spec request_async( name :: client() | data_server(), unit_id(), request(), pid(), non_neg_integer() ) :: :ok
Send an asynchronous request and return immediately.
Use this function when you need non-blocking behavior.
The result is sent to pid as a message.
This function uses GenServer.cast/2, so it always returns :ok even if name is not
running or registered. In that case, no result message is delivered.
Result message format
{:modbuzz, name, unit_id, request, {:ok, response}}
{:modbuzz, name, unit_id, request, {:error, error_response_or_error_reason}}Common failures
- No message received because target process is not started.
{:error, :timeout}in async message payload when response is late.
@spec start_data_server(name :: data_server()) :: :ok | {:error, :already_started}
Start a data server instance.
Returns
:okwhen started.{:error, :already_started}when name is already in use.
Examples
iex> :ok = Modbuzz.start_data_server(:data_server)
iex> alias Modbuzz.PDU.WriteSingleCoil
iex> req = %WriteSingleCoil.Req{output_address: 0 , output_value: true}
iex> res = %WriteSingleCoil.Res{output_address: 0 , output_value: true}
iex> :ok = Modbuzz.create_unit(:data_server, 1)
iex> :ok = Modbuzz.upsert(:data_server, 1, req, res)
iex> {:ok, ^res} = Modbuzz.request(:data_server, 1, req)
@spec start_rtu_client( name :: client(), device_name :: String.t(), transport_opts :: keyword(), transport :: module() ) :: :ok | {:error, :already_started}
Start an RTU client instance.
This function accepts transport_opts as its third argument which allows to pass options to Circuits.UART.
Options provided in transport_opts are passed directly to Circuits.UART without modification.
Returns
:okwhen started.{:error, :already_started}when name is already in use.
Examples
iex> :ok = Modbuzz.start_rtu_client(:client, "ttyUSB0", [speed: 9600])
iex> alias Modbuzz.PDU.WriteSingleCoil
iex> req = %WriteSingleCoil.Req{output_address: 0 , output_value: true}
iex> {:error, %WriteSingleCoil.Err{}} = Modbuzz.request(:client, req)
@spec start_rtu_server( name :: server(), device_name :: String.t(), transport_opts :: Circuits.UART.uart_option(), data_source :: data_server() | client(), transport :: module() ) :: :ok | {:error, :already_started}
Start an RTU server instance.
This function accepts transport_opts as its third argument which allows to pass options to Circuits.UART.
Options provided in transport_opts are passed directly to Circuits.UART without modification.
data_source can be a data server, TCP client, or RTU client process name.
Returns
:okwhen started.{:error, :already_started}when name is already in use.
Examples
iex> :ok = Modbuzz.start_data_server(:data_server)
iex> alias Modbuzz.PDU.WriteSingleCoil
iex> req = %WriteSingleCoil.Req{output_address: 0 , output_value: true}
iex> res = %WriteSingleCoil.Res{output_address: 0 , output_value: true}
iex> :ok = Modbuzz.create_unit(:data_server, 1)
iex> :ok = Modbuzz.upsert(:data_server, 1, req, res)
iex> :ok = Modbuzz.start_rtu_server(:server, "ttyUSB0", [speed: 9600], :data_server)
@spec start_tcp_client( name :: client(), address :: :inet.socket_address() | :inet.hostname(), port :: :inet.port_number() ) :: :ok | {:error, :already_started}
Start a TCP client instance.
Returns
:okwhen started.{:error, :already_started}when name is already in use.
Examples
iex> :ok = Modbuzz.start_tcp_client(:client, {127, 0, 0, 1}, 50200)
iex> alias Modbuzz.PDU.WriteSingleCoil
iex> req = %WriteSingleCoil.Req{output_address: 0 , output_value: true}
iex> {:error, %WriteSingleCoil.Err{}} = Modbuzz.request(:client, req)
@spec start_tcp_server( name :: server(), address :: :inet.socket_address() | :inet.hostname(), port :: :inet.port_number(), data_source :: data_server() | client() ) :: :ok | {:error, :already_started}
Start a TCP server instance.
data_source can be a data server, TCP client, or RTU client process name.
Returns
:okwhen started.{:error, :already_started}when name is already in use.
Examples
iex> :ok = Modbuzz.start_data_server(:data_server)
iex> alias Modbuzz.PDU.WriteSingleCoil
iex> req = %WriteSingleCoil.Req{output_address: 0 , output_value: true}
iex> res = %WriteSingleCoil.Res{output_address: 0 , output_value: true}
iex> :ok = Modbuzz.create_unit(:data_server, 1)
iex> :ok = Modbuzz.upsert(:data_server, 1, req, res)
iex> :ok = Modbuzz.start_tcp_server(:server, {127, 0, 0, 1}, 50200, :data_server)
@spec stop_data_server(name :: data_server()) :: :ok | {:error, :not_started}
Stop a data server instance.
Returns
:okwhen stopped.{:error, :not_started}when target is not running.
@spec stop_rtu_client(name :: client()) :: :ok | {:error, :not_started}
Stop an RTU client instance.
Returns
:okwhen stopped.{:error, :not_started}when target is not running.
@spec stop_rtu_server(name :: server()) :: :ok | {:error, :not_started}
Stop an RTU server instance.
Returns
:okwhen stopped.{:error, :not_started}when target is not running.
@spec stop_tcp_client(name :: client()) :: :ok | {:error, :not_started}
Stop a TCP client instance.
Returns
:okwhen stopped.{:error, :not_started}when target is not running.
@spec stop_tcp_server(name :: server()) :: :ok | {:error, :not_started}
Stop a TCP server instance.
Returns
:okwhen stopped.{:error, :not_started}when target is not running.
@spec upsert(name :: data_server(), unit_id(), request(), response() | callback()) :: :ok | {:error, :unit_not_found}
Upsert a request response/callback pair into a data server unit.
Use this to define how data server responds for a specific request.
When using a callback, callback error handling is your responsibility. If callback fails and does not return a response, caller may observe timeout.
Returns
:okwhen upsert succeeds.{:error, :unit_not_found}when target unit does not exist.