defmodule Modbus.Tcp.Master do
@moduledoc """
TCP Master server.
## Example
```elixir
#run with: mix opto22
alias Modbus.Tcp.Master
# opto22 rack configured as follows
# m0 - 4p digital input
# p0 - 24V
# p1 - 0V
# p2 - m1.p2
# p3 - m1.p3
# m1 - 4p digital output
# p0 - NC
# p1 - NC
# p2 - m0.p2
# p3 - m0.p3
# m2 - 2p analog input (-10V to +10V)
# p0 - m3.p0
# p1 - m3.p1
# m3 - 2p analog output (-10V to +10V)
# p0 - m2.p0
# p1 - m2.p1
{:ok, pid} = Master.start_link(ip: {10,77,0,10}, port: 502)
#turn off m1.p0
:ok = Master.exec(pid, {:fc, 1, 4, 0})
#turn on m1.p1
:ok = Master.exec(pid, {:fc, 1, 5, 1})
#alternate m1.p2 and m1.p3
:ok = Master.exec(pid, {:fc, 1, 6, [1, 0]})
#https://www.h-schmidt.net/FloatConverter/IEEE754.html
#write -5V (IEEE 754 float) to m3.p0
#<<-5::float-32>> -> <<192, 160, 0, 0>>
:ok = Master.exec(pid, {:phr, 1, 24, [0xc0a0, 0x0000]})
:ok = Master.exec(pid, {:phr, 1, 24, Modbus.IEEE754.to_2_regs(-5.0, :be)})
#write +5V (IEEE 754 float) to m3.p1
#<<+5::float-32>> -> <<64, 160, 0, 0>>
:ok = Master.exec(pid, {:phr, 1, 26, [0x40a0, 0x0000]})
:ok = Master.exec(pid, {:phr, 1, 26, Modbus.IEEE754.to_2_regs(+5.0, :be)})
:timer.sleep(20) #outputs settle delay
#read previous coils as inputs
{:ok, [0, 1, 1, 0]} = Master.exec(pid, {:ri, 1, 4, 4})
#read previous analog channels as input registers
{:ok, [0xc0a0, 0x0000, 0x40a0, 0x0000]} = Master.exec(pid, {:rir, 1, 24, 4})
{:ok, data} = Master.exec(pid, {:rir, 1, 24, 4})
[-5.0, +5.0] = Modbus.IEEE754.from_2n_regs(data, :be)
```
"""
use GenServer
alias Modbus.Tcp
@to 2000
##########################################
# Public API
##########################################
@doc """
Starts the Server.
`state` is a keyword list where:
`ip` is the internet address to connect to.
`port` is the tcp port number to connect to.
`timeout` is the connection timeout.
Returns `{:ok, pid}`.
## Example
```elixir
Modbus.Tcp.Master.new(ip: {10,77,0,10}, port: 502, timeout: 2000)
```
"""
def start_link(opts) do
ip = Keyword.fetch!(opts, :ip)
port = Keyword.fetch!(opts, :port)
timeout = Keyword.get(opts, :timeout, @to)
GenServer.start_link(__MODULE__, {ip, port, timeout})
end
def init({ip, port, timeout}) do
opts = [:binary, packet: :raw, active: false]
result = :gen_tcp.connect(ip, port, opts, timeout)
case result do
{:ok, socket} ->
{:ok, {socket, 0}}
{:error, reason} ->
{:stop, reason}
end
end
@doc """
Stops the Server.
"""
def stop(pid) do
GenServer.stop(pid)
end
@doc """
Executes a Modbus TCP 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]}`.
"""
def exec(pid, cmd, timeout \\ @to) do
GenServer.call(pid, {:exec, cmd, timeout})
end
def handle_call({:exec, cmd, timeout}, _from, {socket, transid}) do
request = Tcp.pack_req(cmd, transid)
length = Tcp.res_len(cmd)
case :gen_tcp.send(socket, request) do
{:error, reason} ->
{:stop, reason}
:ok ->
case :gen_tcp.recv(socket, length, timeout) do
{:ok, response} ->
values = Tcp.parse_res(cmd, response, transid)
case values do
nil -> {:reply, :ok, {socket, transid + 1}}
_ -> {:reply, {:ok, values}, {socket, transid + 1}}
end
{:error, reason} ->
{:stop, reason}
end
end
end
end