defmodule Modbus.Master do
@moduledoc """
Modbus Master.
## Example
```elixir
# run with: mix slave
alias Modbus.Slave
alias Modbus.Master
# start your slave with a shared model
model = %{
0x50 => %{
{:c, 0x5152} => 0,
{:i, 0x5354} => 0,
{:i, 0x5355} => 1,
{:hr, 0x5657} => 0x6162,
{:ir, 0x5859} => 0x6364,
{:ir, 0x585A} => 0x6566
}
}
{:ok, slave} = Slave.start_link(model: model)
# get the assigned tcp port
port = Slave.port(slave)
# interact with it
{:ok, master} = Master.start_link(ip: {127, 0, 0, 1}, port: port)
# read input
{:ok, [0, 1]} = Master.exec(master, {:ri, 0x50, 0x5354, 2})
# read input registers
{:ok, [0x6364, 0x6566]} = Master.exec(master, {:rir, 0x50, 0x5859, 2})
# toggle coil and read it back
:ok = Master.exec(master, {:fc, 0x50, 0x5152, 0})
{:ok, [0]} = Master.exec(master, {:rc, 0x50, 0x5152, 1})
:ok = Master.exec(master, {:fc, 0x50, 0x5152, 1})
{:ok, [1]} = Master.exec(master, {:rc, 0x50, 0x5152, 1})
# increment holding register and read it back
{:ok, [0x6162]} = Master.exec(master, {:rhr, 0x50, 0x5657, 1})
:ok = Master.exec(master, {:phr, 0x50, 0x5657, 0x6163})
{:ok, [0x6163]} = Master.exec(master, {:rhr, 0x50, 0x5657, 1})
:ok = Master.stop(master)
:ok = Slave.stop(slave)
```
"""
alias Modbus.Transport
alias Modbus.Protocol
@to 2000
##########################################
# Public API
##########################################
@doc """
Opens the connection.
`opts` is a keyword list where:
- `trans` is the transport to use. Only the `:tcp` transport is available but other transports can be registered.
- `proto` is the protocol to use. Available protocols are `:tcp` and `:rtu`. Defaults to `:tcp`.
The rest of options are passed verbatim to the transport constructor.
The following are the options needed by the default TCP transport.
- `ip` is the internet address to connect to.
- `port` is the tcp port number to connect to.
- `timeout` is the optional connection timeout.
Returns `{:ok, master}` | `{:error, reason}`.
## Example
```elixir
Modbus.Master.start_link(ip: {10,77,0,10}, port: 502, timeout: 2000)
```
"""
def start_link(opts) do
transm = Keyword.get(opts, :trans, Modbus.Tcp.Transport)
protom = Keyword.get(opts, :proto, Modbus.Tcp.Protocol)
tid = Protocol.next(protom, nil)
init = %{trans: transm, proto: protom, opts: opts, tid: tid}
GenServer.start_link(__MODULE__.Server, init)
end
@doc """
Closes the connection.
Returns `:ok` | `{:error, reason}`.
"""
def stop(master) do
GenServer.stop(master)
end
@doc """
Executes a Modbus command.
`cmd` is one of:
- `{:rc, slave, address, count}` read `count` coils.
- `{:ri, slave, address, count}` read `count` inputs.
- `{:rhr, slave, address, count}` read `count` holding registers.
- `{:rir, slave, address, count}` read `count` input registers.
- `{:fc, slave, address, value}` force single coil.
- `{:phr, slave, address, value}` preset single holding register.
- `{:fc, slave, address, values}` force multiple coils.
- `{:phr, slave, address, values}` preset multiple holding registers.
Returns `:ok` | `{:ok, [values]}` | `{:error, reason}`.
"""
def exec(master, cmd, timeout \\ @to)
when is_tuple(cmd) and is_integer(timeout) do
GenServer.call(master, {:exec, cmd, timeout})
end
defmodule Server do
@moduledoc false
use GenServer
def init(%{trans: transm, opts: opts} = init) do
case Transport.open(transm, opts) do
{:ok, transi} ->
transp = {transm, transi}
{:ok, %{trans: transp, proto: init.proto, tid: init.tid}}
{:error, reason} ->
{:stop, reason}
end
end
def terminate(_reason, %{trans: trans}) do
Transport.close(trans)
end
def handle_call({:get, :tid}, _from, state) do
{:reply, state.tid, state}
end
def handle_call({:update, :tid, tid}, _from, state) do
{:reply, :ok, Map.put(state, :tid, tid)}
end
def handle_call({:exec, cmd, timeout}, _from, state) do
%{trans: trans, proto: proto, tid: tid} = state
state = Map.put(state, :tid, Protocol.next(state.proto, tid))
result =
case request(proto, cmd, tid) do
{:ok, request, length} ->
case Transport.write(trans, request) do
:ok ->
case Transport.readn(trans, length, timeout) do
{:ok, response} ->
values = Protocol.parse_res(proto, cmd, response, tid)
case values do
nil -> :ok
_ -> {:ok, values}
end
error ->
error
end
error ->
error
end
error ->
error
end
{:reply, result, state}
end
defp request(proto, cmd, tid) do
try do
request = Protocol.pack_req(proto, cmd, tid)
length = Protocol.res_len(proto, cmd)
{:ok, request, length}
rescue
_ ->
{:error, {:invalid, cmd}}
end
end
end
end