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