[erlang-questions] Modbus/TCP anyone?
Enrique Marcote Peña
mpquique@REDACTED
Tue Apr 1 20:20:14 CEST 2008
Thank you very much Stefan,
I will test it against an ADAM 6060 (data acquisition and control
module) and let you know. UDP should suit me also, I believe this
device supports both. Thanks again.
Cheers,
Quique
El 01/04/2008, a las 19:47, Stefan Nickl escribió:
> Enrique Marcote Peña wrote:
>> Hi,
>> Is there any erlang Modbus/TCP implementation available?
>
> I've hacked up some code for this, progressing at a rate of
> about 20 lines per christmas season when I got some time :)
>
> Actually it uses UDP, but I've used TCP before and switched
> to UDP since I feel that's the more natural thing for modbus.
>
> Testing against a Kontron ThinkIO = Wago I/O-IPC.
>
> Regards,
> Stefan
> %%%-------------------------------------------------------------------
> %%% File : modbus_tcp.erl
> %%% Author : <snickl@REDACTED>
> %%% Description :
> %%%
> %%% Created : 29 Dec 2004 by <snickl@REDACTED>
> %%%-------------------------------------------------------------------
> -module(modbus).
>
> -behaviour(gen_fsm).
>
> %% API
> -export([start_link/2]).
>
> %% gen_fsm callbacks
> -export([init/1,
> state_process_request/3,
> state_wait_for_response/2, state_process_response/2,
> handle_event/3, handle_sync_event/4, handle_info/3,
> terminate/3, code_change/4,
> read_reg/1, read_reg/2, write_reg/2, add_clamp/5, get_clamp_addr/
> 1]).
>
> -record(state, {sock, host, port, caller, quantity, tid=0, reply,
> clamps=[]}).
>
> -record(clamp, {name, in_words, out_words, in_coils, out_coils}).
>
> -define(TIMEOUT, 3000).
>
> -define(SERVER, {local, ?MODULE}).
> -define(REF, ?MODULE).
>
> -define(FC_READ_COILS, 16#01).
> -define(FC_WRITE_COILS, 16#0f).
> -define(FC_READ_REGS, 16#03).
> -define(FC_WRITE_REGS, 16#10).
> -define(FC_GET_COM_EVENT, 16#0b).
>
> %%====================================================================
> %% API
> %%====================================================================
> %%--------------------------------------------------------------------
> %% Function: start_link() -> ok,Pid} | ignore | {error,Error}
> %% Description:Creates a gen_fsm process which calls Module:init/1 to
> %% initialize. To ensure a synchronized start-up procedure, this
> function
> %% does not return until Module:init/1 has returned.
> %%--------------------------------------------------------------------
> start_link(Host, Port) ->
> gen_fsm:start_link(?SERVER, ?MODULE, [Host, Port],
> [{debug, [trace]}]).
>
> add_clamp(Name, InWords, OutWords, InCoils, OutCoils) ->
> gen_fsm:sync_send_all_state_event(?REF,
> {add_clamp, InWords, OutWords, InCoils, OutCoils}).
> get_clamp_addr(Name) ->
> gen_fsm:sync_send_all_state_event(?REF,
> {get_clamp_addr, Name}).
>
> read_reg(Offset) ->
> read_reg(Offset, 1).
> read_reg(Offset, Length) ->
> Reply = gen_fsm:sync_send_event(?REF,
> {mb_read_regs, Offset, Length}),
> %io:format("reg 0x~s: ~w~n", [httpd_util:encode_hex(Offset),
> Reply]),
> {mb_read_regs, ValuesList} = Reply,
> {ok, ValuesList}.
>
> % thinkio seems not to support readback from 0x200 +x, implement
> shadow
> % registers with a dict sometime.
> write_reg(Offset, ValuesList) ->
> Reply = gen_fsm:sync_send_event(?REF,
> {mb_write_regs, Offset, ValuesList}),
> timer:sleep(10), % 4-5ms seem to be maximum for reality to
> catch up
> {mb_write_regs, _Start, _Quantity} = Reply,
> {ok, ValuesList}.
> % io:format("reg 0x~s: ~w~n", [httpd_util:encode_hex(Offset),
> Reply]).
>
> %%====================================================================
> %% gen_fsm callbacks
> %%====================================================================
> %%--------------------------------------------------------------------
> %% Function: init(Args) -> {ok, StateName, State} |
> %% {ok, StateName, State, Timeout} |
> %% ignore |
> %% {stop, StopReason}
> %% Description:Whenever a gen_fsm is started using gen_fsm:start/
> [3,4] or
> %% gen_fsm:start_link/3,4, this function is called by the new
> process to
> %% initialize.
> %%--------------------------------------------------------------------
> init([Host, Port]) ->
> {ok, Sock} = gen_udp:open(0, [binary, inet]),
> timer:send_interval(1000, tick),
> {ok, state_process_request, #state{sock=Sock, host=Host,
> port=Port}}.
>
> %%--------------------------------------------------------------------
> %% Function:
> %% state_name(Event, State) -> {next_state, NextStateName, NextState}|
> %% {next_state, NextStateName,
> %% NextState, Timeout} |
> %% {stop, Reason, NewState}
> %% Description:There should be one instance of this function for
> each possible
> %% state name. Whenever a gen_fsm receives an event sent using
> %% gen_fsm:send_event/2, the instance of this function with the
> same name as
> %% the current state name StateName is called to handle the event.
> It is also
> %% called if a timeout occurs.
> %%--------------------------------------------------------------------
> %state_name(_Event, State) ->
> % {next_state, state_name, State}.
>
> state_wait_for_response(Event, State) ->
> io:format("entered wait_for_response: ~w~n", [Event]),
> case Event of
> {timeout, _} ->
> io:format("timeout: no response from server~n")
> end,
> {next_state, state_process_request, State}.
>
> state_process_response(Event, State) ->
> Data = Event,
> %Data = State#state.reply,
> io:format("response: ~w~n", [Data]),
> <<TID:16, PID:16, _Length:16, UID:8, Fc, T/binary>> = Data,
> TID = State#state.tid, % checks
> PID = 0,
> UID = 16#ff,
> Reply = if
> Fc < 128 ->
> case Fc of
> ?FC_READ_COILS ->
> <<_ByteCount, Values/binary>> = T,
> {mb_read_coils, State#state.quantity, Values};
> ?FC_WRITE_COILS ->
> <<Start:16, Quantity:16>> = T,
> Quantity = State#state.quantity, % check
> {mb_write_coils, Start, Quantity};
> ?FC_READ_REGS ->
> <<_ByteCount, Values/binary>> = T,
> {mb_read_regs, binary_to_word16_list(Values)};
> ?FC_WRITE_REGS ->
> <<Start:16, Quantity:16>> = T,
> Quantity = State#state.quantity, % check
> {mb_write_regs, Start, Quantity};
> _ ->
> {error, function_not_implemented}
> end;
> true ->
> <<ExCode>> = T,
> case ExCode of
> 1 -> {error, function_not_implemented_by_server};
> 2 -> {error, invalid_address};
> 3 -> {error, invalid_data_value};
> _ -> {error, other_error}
> end
> end,
> gen_fsm:reply(State#state.caller, Reply),
> {next_state, state_process_request, State}.
>
> %%--------------------------------------------------------------------
> %% Function:
> %% state_name(Event, From, State) -> {next_state, NextStateName,
> NextState} |
> %% {next_state, NextStateName,
> %% NextState, Timeout} |
> %% {reply, Reply, NextStateName,
> NextState}|
> %% {reply, Reply, NextStateName,
> %% NextState, Timeout} |
> %% {stop, Reason, NewState}|
> %% {stop, Reason, Reply, NewState}
> %% Description: There should be one instance of this function for each
> %% possible state name. Whenever a gen_fsm receives an event sent
> using
> %% gen_fsm:sync_send_event/2,3, the instance of this function with
> the same
> %% name as the current state name StateName is called to handle the
> event.
> %%--------------------------------------------------------------------
> %state_name(_Event, _From, State) ->
> % Reply = ok,
> % {reply, Reply, state_name, State}.
>
>
> state_process_request(Event, From, StateIn) ->
> State = StateIn#state{caller=From},
> TID = StateIn#state.tid + 1, PID = 0, UID = 16#ff,
> T = case Event of
> {mb_read_coils, Start, Quantity} ->
> Length = 6,
> State2 = State#state{quantity=Quantity, tid=TID},
> <<?FC_READ_COILS,
> Start:16, Quantity:16>>;
> {mb_write_coils, Start, Quantity, Values} when binary(Values) ->
> ByteCount = length(binary_to_list(Values)),
> Length = 7 + ByteCount,
> State2 = State#state{quantity=Quantity, tid=TID},
> <<?FC_WRITE_COILS,
> Start:16, Quantity:16, ByteCount, Values/binary>>;
> {mb_read_regs, Start, Quantity} ->
> Length = 6,
> State2 = State#state{quantity=Quantity, tid=TID},
> <<?FC_READ_REGS,
> Start:16, Quantity:16>>;
> {mb_write_regs, Start, ValuesList} when list(ValuesList) ->
> ValuesBin = list_word16_to_binary(ValuesList),
> ByteCount = length(binary_to_list(ValuesBin)),
> Length = 7 + ByteCount,
> Quantity = trunc(ByteCount / 2), % check
> State2 = State#state{quantity=Quantity, tid=TID},
> <<?FC_WRITE_REGS,
> Start:16, Quantity:16, ByteCount, ValuesBin/binary>>;
> _ ->
> Quantity = 0,
> Length = 0,
> State2 = State,
> function_not_implemented
> end,
> case T of
> function_not_implemented ->
> io:format("error: function is not implemented~n"),
> gen_fsm:reply(State2#state.caller,
> {error, function_not_implemented}),
> {next_state, state_process_request, State2};
> T ->
> if
> Quantity < 1; Quantity > 2000 ->
> gen_fsm:reply(State2#state.caller,
> {error, invalid_quantity}),
> {next_state, state_process_request, State2};
> true ->
> Request = concat_binary([<<TID:16,PID:16,Length:16,UID:8>>, T]),
> io:format("request: ~w~n", [Request]),
> ok = gen_udp:send(State#state.sock, State#state.host,
> State#state.port, Request),
> {next_state, state_wait_for_response, State2, ?TIMEOUT}
> end
> end.
>
> %%--------------------------------------------------------------------
> %% Function:
> %% handle_event(Event, StateName, State) -> {next_state,
> NextStateName,
> %% NextState} |
> %% {next_state,
> NextStateName,
> %% NextState, Timeout} |
> %% {stop, Reason, NewState}
> %% Description: Whenever a gen_fsm receives an event sent using
> %% gen_fsm:send_all_state_event/2, this function is called to handle
> %% the event.
> %%--------------------------------------------------------------------
> handle_event(_Event, StateName, State) ->
> {next_state, StateName, State}.
>
> %%--------------------------------------------------------------------
> %% Function:
> %% handle_sync_event(Event, From, StateName,
> %% State) -> {next_state, NextStateName,
> NextState} |
> %% {next_state, NextStateName, NextState,
> %% Timeout} |
> %% {reply, Reply, NextStateName,
> NextState}|
> %% {reply, Reply, NextStateName,
> NextState,
> %% Timeout} |
> %% {stop, Reason, NewState} |
> %% {stop, Reason, Reply, NewState}
> %% Description: Whenever a gen_fsm receives an event sent using
> %% gen_fsm:sync_send_all_state_event/2,3, this function is called
> to handle
> %% the event.
> %%--------------------------------------------------------------------
> handle_sync_event(Event, _From, StateName, State) ->
> io:format("in handle_sync_event: ~w~n", [Event]),
> case Event of
> {add_clamp, Data} ->
> State2 = State#state{clamps = State#state.clamps ++ [Data]};
> {get_clamp_addr, Name} ->
> %calc_clamp_addr(Name, State#state.clamps),
> State2 = State
> end,
> Reply = ok,
> io:format("in handle_sync_event1: ~w~n", [State2]),
> {reply, Reply, StateName, State2}.
>
> %%--------------------------------------------------------------------
> %% Function:
> %% handle_info(Info,StateName,State)-> {next_state, NextStateName,
> NextState}|
> %% {next_state, NextStateName,
> NextState,
> %% Timeout} |
> %% {stop, Reason, NewState}
> %% Description: This function is called by a gen_fsm when it
> receives any
> %% other message than a synchronous or asynchronous event
> %% (or a system message).
> %%--------------------------------------------------------------------
> handle_info(Info, _StateName, State) ->
> case Info of
> {udp, _Socket, _IP, _InPort_No, Packet} ->
> gen_fsm:send_event(?REF, Packet),
> {next_state, state_process_response, State}; %, State#state
> {reply=Packet}};
> tick ->
> % poll clamps
> io:format("got tick~n");
> _ ->
> {stop, connection_closed, State}
> end.
>
> %%--------------------------------------------------------------------
> %% Function: terminate(Reason, StateName, State) -> void()
> %% Description:This function is called by a gen_fsm when it is about
> %% to terminate. It should be the opposite of Module:init/1 and do any
> %% necessary cleaning up. When it returns, the gen_fsm terminates with
> %% Reason. The return value is ignored.
> %%--------------------------------------------------------------------
> terminate(_Reason, _StateName, State) ->
> ok = gen_udp:close(State#state.sock),
> ok.
>
> %%--------------------------------------------------------------------
> %% Function:
> %% code_change(OldVsn, StateName, State, Extra) -> {ok, StateName,
> NewState}
> %% Description: Convert process state when code is changed
> %%--------------------------------------------------------------------
> code_change(_OldVsn, StateName, State, _Extra) ->
> {ok, StateName, State}.
>
> %%--------------------------------------------------------------------
> %%% Internal functions
> %%--------------------------------------------------------------------
>
> binary_to_word16_list(Values) when binary(Values) ->
> binary_to_word16_list(binary_to_list(Values));
> binary_to_word16_list([X, Y |T]) ->
> [X bsl 8 + Y |binary_to_word16_list(T)];
> binary_to_word16_list([]) -> [].
>
> list_word16_to_binary(Values) when list(Values) ->
> concat_binary([<<X:16>> || X <- Values]).
>
> %calc_clamp_addr(Name, Clamps) ->
> % calc_clamp_addr(Name, Clamps, []).
> % AOsum = #clamp{},
> %calc_clamp_addr(Name, [H|T], Acc) ->
> % io:format("list: ~w~n", [H]),
>
> % AOsum#clamp.in_words = AOsum#clamp.in_words + H#clamp.in_words,
> % calc_clamp_addr(Name, T, Acc);
> %calc_clamp_addr(Name, [], Acc) ->
> % Acc.
> %%%-------------------------------------------------------------------
> %%% File : test_mt.erl
> %%% Author : <snickl@REDACTED>
> %%% Description :
> %%%
> %%% Created : 29 Dec 2004 by <snickl@REDACTED>
> %%%-------------------------------------------------------------------
> -module(test_mt).
>
> -export([start/0, check_reg/2, check_comm/0]).
> -import(modbus, [read_reg/1, read_reg/2, write_reg/2,
> add_clamp/5, get_clamp_addr/1]).
>
> check_comm() ->
> check_reg(16#2000, 16#0000),
> check_reg(16#2001, 16#ffff),
> check_reg(16#2002, 16#1234),
> check_reg(16#2003, 16#aaaa),
> check_reg(16#2004, 16#5555),
> read_reg(16#2015). % software index
>
> check_reg(Offset, Value) ->
> io:format("checking reg 0x~s for value: 0x~s~n",
> [httpd_util:encode_hex(Offset), httpd_util:encode_hex(Value)]),
> Reply = gen_fsm:sync_send_event(modbus_udp, {mb_read_regs,
> Offset, 1}),
> {mb_read_regs, 1, [Value]} = Reply.
>
> inout_test() ->
> [{write_reg(0, [X]), read_reg(0)} || X <- lists:seq(30000, 0,
> -3000)].
>
> start() ->
> %{ok,_Pid} = modbus_udp:start_link("thinkio", 502),
> modbus:start_link("thinkio", 502),
> %Reply = gen_fsm:sync_send_event(modbus_udp, {mb_read_coils, 0,
> 1}),
> %io:format("reply: ~w~n", [Reply]),
> %check_comm(),
> %write_reg(0, [16#3000]),
> read_reg(16#0),
> %inout_test().
> add_clamp(ai2_467, 2, 0, 0, 0),
> add_clamp(ao2_550, 0, 2, 0, 0),
> get_clamp_addr(ai2_467),
> get_clamp_addr(ao2_550).
More information about the erlang-questions
mailing list