Modbuzz (Modbuzz v0.3.0)

Copy Markdown View Source

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:

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 a TCP client 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

callback()

@type callback() :: (request() -> response())

client()

@type client() :: GenServer.name()

data_server()

@type data_server() :: GenServer.name()

error()

@type error() :: Modbuzz.PDU.Protocol.t()

request()

@type request() :: Modbuzz.PDU.Protocol.t()

response()

@type response() :: Modbuzz.PDU.Protocol.t()

server()

@type server() :: GenServer.name()

unit_id()

@type unit_id() :: 0..247

Functions

create_unit(name, unit_id \\ 0)

@spec create_unit(name :: data_server(), unit_id()) ::
  :ok | {:error, :already_created}

Create a unit under a data server.

Returns

  • :ok when unit is created.
  • {:error, :already_created} when the unit already exists.

delete(name, unit_id \\ 0, request)

@spec delete(name :: data_server(), unit_id(), request()) ::
  :ok | {:error, :unit_not_found}

Delete a request mapping from a data server unit.

Returns

  • :ok when mapping is deleted.
  • {:error, :unit_not_found} when target unit does not exist.

dump(name, unit_id \\ 0)

@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.

request(name, unit_id \\ 0, request, timeout \\ 5000)

@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 before timeout.
  • Caller exits with :noproc if target process is not running (GenServer.call/3 behavior).

request_async(name, unit_id \\ 0, request, pid \\ self(), timeout \\ 5000)

@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.

start_data_server(name)

@spec start_data_server(name :: data_server()) :: :ok | {:error, :already_started}

Start a data server instance.

Returns

  • :ok when 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)

start_rtu_client(name, device_name, transport_opts, transport \\ Circuits.UART)

@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

  • :ok when 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)

start_rtu_server(name, device_name, transport_opts, data_source, transport \\ Circuits.UART)

@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

  • :ok when 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)

start_tcp_client(name, address, port)

@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

  • :ok when 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)

start_tcp_server(name, address, port, data_source)

@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

  • :ok when 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)

stop_data_server(name)

@spec stop_data_server(name :: data_server()) :: :ok | {:error, :not_started}

Stop a data server instance.

Returns

  • :ok when stopped.
  • {:error, :not_started} when target is not running.

stop_rtu_client(name)

@spec stop_rtu_client(name :: client()) :: :ok | {:error, :not_started}

Stop an RTU client instance.

Returns

  • :ok when stopped.
  • {:error, :not_started} when target is not running.

stop_rtu_server(name)

@spec stop_rtu_server(name :: server()) :: :ok | {:error, :not_started}

Stop an RTU server instance.

Returns

  • :ok when stopped.
  • {:error, :not_started} when target is not running.

stop_tcp_client(name)

@spec stop_tcp_client(name :: client()) :: :ok | {:error, :not_started}

Stop a TCP client instance.

Returns

  • :ok when stopped.
  • {:error, :not_started} when target is not running.

stop_tcp_server(name)

@spec stop_tcp_server(name :: server()) :: :ok | {:error, :not_started}

Stop a TCP server instance.

Returns

  • :ok when stopped.
  • {:error, :not_started} when target is not running.

upsert(name, unit_id \\ 0, request, res_or_cb)

@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

  • :ok when upsert succeeds.
  • {:error, :unit_not_found} when target unit does not exist.