[erlang-questions] Modbus/TCP anyone?

Enrique Marcote Peña <>
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  :  <>
> %%% Description :
> %%%
> %%% Created : 29 Dec 2004 by  <>
> %%%-------------------------------------------------------------------
> -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  :  <>
> %%% Description :
> %%%
> %%% Created : 29 Dec 2004 by  <>
> %%%-------------------------------------------------------------------
> -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