rmodbus-ccutrer 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/NEWS.md +180 -0
  3. data/README.md +115 -0
  4. data/Rakefile +29 -0
  5. data/examples/perfomance_rtu.rb +56 -0
  6. data/examples/perfomance_rtu_via_tcp.rb +55 -0
  7. data/examples/perfomance_tcp.rb +55 -0
  8. data/examples/simple-xpca-gateway.rb +84 -0
  9. data/examples/use_rtu_via_tcp_modbus.rb +22 -0
  10. data/examples/use_tcp_modbus.rb +23 -0
  11. data/lib/rmodbus.rb +21 -0
  12. data/lib/rmodbus/client.rb +94 -0
  13. data/lib/rmodbus/client/slave.rb +345 -0
  14. data/lib/rmodbus/debug.rb +25 -0
  15. data/lib/rmodbus/errors.rb +42 -0
  16. data/lib/rmodbus/ext.rb +85 -0
  17. data/lib/rmodbus/options.rb +6 -0
  18. data/lib/rmodbus/proxy.rb +41 -0
  19. data/lib/rmodbus/rtu.rb +122 -0
  20. data/lib/rmodbus/rtu_client.rb +43 -0
  21. data/lib/rmodbus/rtu_server.rb +48 -0
  22. data/lib/rmodbus/rtu_slave.rb +48 -0
  23. data/lib/rmodbus/rtu_via_tcp_server.rb +35 -0
  24. data/lib/rmodbus/server.rb +246 -0
  25. data/lib/rmodbus/server/slave.rb +16 -0
  26. data/lib/rmodbus/sp.rb +36 -0
  27. data/lib/rmodbus/tcp.rb +31 -0
  28. data/lib/rmodbus/tcp_client.rb +25 -0
  29. data/lib/rmodbus/tcp_server.rb +67 -0
  30. data/lib/rmodbus/tcp_slave.rb +55 -0
  31. data/lib/rmodbus/version.rb +3 -0
  32. data/spec/client_spec.rb +88 -0
  33. data/spec/exception_spec.rb +120 -0
  34. data/spec/ext_spec.rb +52 -0
  35. data/spec/logging_spec.rb +89 -0
  36. data/spec/proxy_spec.rb +74 -0
  37. data/spec/read_rtu_response_spec.rb +92 -0
  38. data/spec/response_mismach_spec.rb +163 -0
  39. data/spec/rtu_client_spec.rb +86 -0
  40. data/spec/rtu_server_spec.rb +31 -0
  41. data/spec/rtu_via_tcp_client_spec.rb +76 -0
  42. data/spec/rtu_via_tcp_server_spec.rb +89 -0
  43. data/spec/slave_spec.rb +55 -0
  44. data/spec/spec_helper.rb +54 -0
  45. data/spec/tcp_client_spec.rb +88 -0
  46. data/spec/tcp_server_spec.rb +158 -0
  47. metadata +206 -0
@@ -0,0 +1,89 @@
1
+ # -*- coding: ascii
2
+ require "rmodbus"
3
+
4
+ describe ModBus::RTUViaTCPServer do
5
+ before :all do
6
+ @port = 8502
7
+ begin
8
+ @server = ModBus::RTUViaTCPServer.new(@port)
9
+ @server_slave = @server.with_slave(1)
10
+ @server_slave.coils = [1,0,1,1]
11
+ @server_slave.discrete_inputs = [1,1,0,0]
12
+ @server_slave.holding_registers = [1,2,3,4]
13
+ @server_slave.input_registers = [1,2,3,4]
14
+ @server.promiscuous = true
15
+ @server.start
16
+ rescue Errno::EADDRINUSE
17
+ @port += 1
18
+ retry
19
+ end
20
+ @cl = ModBus::RTUClient.new('127.0.0.1', @port)
21
+ @cl.read_retries = 1
22
+ @slave = @cl.with_slave(1)
23
+ # pretend this is a serialport and we're just someone else on the same bus
24
+ @io = @cl.instance_variable_get(:@io)
25
+ end
26
+
27
+ before do
28
+ @server.debug = false
29
+ end
30
+
31
+ it "should have options :host" do
32
+ host = '192.168.0.1'
33
+ srv = ModBus::RTUViaTCPServer.new(1010, :host => '192.168.0.1')
34
+ srv.host.should eql(host)
35
+ end
36
+
37
+ it "should have options :max_connection" do
38
+ max_conn = 5
39
+ srv = ModBus::RTUViaTCPServer.new(1010, :max_connection => 5)
40
+ srv.maxConnections.should eql(max_conn)
41
+ end
42
+
43
+ it "should properly ignore responses from other slaves" do
44
+ request = "\x10\x03\x0\x1\x0\x1\xd6\x8b"
45
+ response = "\x10\x83\x1\xd0\xf5"
46
+ @server.debug = true
47
+ @server.should receive(:log).ordered.with("Server RX (8 bytes): [10][03][00][01][00][01][d6][8b]")
48
+ @server.should receive(:log).ordered.with("Server RX function 3 to 16: {:quant=>1, :addr=>1}")
49
+ @server.should receive(:log).ordered.with("Server RX (5 bytes): [10][83][01][d0][f5]")
50
+ @server.should receive(:log).ordered.with("Server RX response 3 from 16: {:err=>1}")
51
+ @server.should receive(:log).ordered.with("Server RX (8 bytes): [01][01][00][00][00][01][fd][ca]")
52
+ @server.should receive(:log).ordered.with("Server RX function 1 to 1: {:quant=>1, :addr=>0}")
53
+ @server.should receive(:log).ordered.with("Server TX (6 bytes): [01][01][01][01][90][48]")
54
+ @io.write(request)
55
+ @io.write(response)
56
+ # just to prove the server can still handle subsequent requests
57
+ @slave.read_coils(0, 1).should == [1]
58
+ end
59
+
60
+ it "should properly ignore functions from other slaves that it doesn't understand" do
61
+ request = "\x10\x41\x0\x1\x0\x1\x0\x5\x0\x1\xb1\x00"
62
+ response = "\x10\xc1\x1\xe0\x55"
63
+ @io.write(request)
64
+ @io.write(response)
65
+ # just to prove the server can still handle subsequent requests
66
+ @slave.read_coils(0, 1).should == [1]
67
+ end
68
+
69
+ it "should properly ignore utter garbage on the line from starting up halfway through a conversation" do
70
+ response = "garbage" * 50 + "\x1\x55\xe0"
71
+ @io.write(response)
72
+ # just to prove the server can still handle subsequent requests
73
+ @slave.read_coils(0, 1).should == [1]
74
+ end
75
+
76
+ it "should send exception if request is malformed" do
77
+ lambda { @slave.query("\x01\x01") }.should raise_exception(
78
+ ModBus::Errors::ModBusTimeout)
79
+ end
80
+
81
+ after :all do
82
+ @cl.close unless @cl.closed?
83
+ @server.stop unless @server.stopped?
84
+ while GServer.in_service?(@port)
85
+ sleep(0.01)
86
+ end
87
+ @server.stop
88
+ end
89
+ end
@@ -0,0 +1,55 @@
1
+ # -*- coding: ascii
2
+ require 'rmodbus'
3
+
4
+ describe ModBus::Client::Slave do
5
+ before do
6
+ @slave = ModBus::Client.new.with_slave(1)
7
+
8
+ @slave.stub(:query).and_return('')
9
+ end
10
+
11
+ it "should support function 'read coils'" do
12
+ @slave.should_receive(:query).with("\x1\x0\x13\x0\x13").and_return("\xcd\x6b\x5")
13
+ @slave.read_coils(0x13,0x13).should == [1,0,1,1, 0,0,1,1, 1,1,0,1, 0,1,1,0, 1,0,1]
14
+ end
15
+
16
+ it "should support function 'read discrete inputs'" do
17
+ @slave.should_receive(:query).with("\x2\x0\xc4\x0\x16").and_return("\xac\xdb\x35")
18
+ @slave.read_discrete_inputs(0xc4,0x16).should == [0,0,1,1, 0,1,0,1, 1,1,0,1, 1,0,1,1, 1,0,1,0, 1,1]
19
+ end
20
+
21
+ it "should support function 'read holding registers'" do
22
+ @slave.should_receive(:query).with("\x3\x0\x6b\x0\x3").and_return("\x2\x2b\x0\x0\x0\x64")
23
+ @slave.read_holding_registers(0x6b,0x3).should == [0x022b, 0x0000, 0x0064]
24
+ end
25
+
26
+ it "should support function 'read input registers'" do
27
+ @slave.should_receive(:query).with("\x4\x0\x8\x0\x1").and_return("\x0\xa")
28
+ @slave.read_input_registers(0x8,0x1).should == [0x000a]
29
+ end
30
+
31
+ it "should support function 'write single coil'" do
32
+ @slave.should_receive(:query).with("\x5\x0\xac\xff\x0").and_return("\xac\xff\x00")
33
+ @slave.write_single_coil(0xac,0x1).should == @slave
34
+ end
35
+
36
+ it "should support function 'write single register'" do
37
+ @slave.should_receive(:query).with("\x6\x0\x1\x0\x3").and_return("\x1\x0\x3")
38
+ @slave.write_single_register(0x1,0x3).should == @slave
39
+ end
40
+
41
+ it "should support function 'write multiple coils'" do
42
+ @slave.should_receive(:query).with("\xf\x0\x13\x0\xa\x2\xcd\x1").and_return("\x13\x0\xa")
43
+ @slave.write_multiple_coils(0x13,[1,0,1,1, 0,0,1,1, 1,0]).should == @slave
44
+ end
45
+
46
+ it "should support function 'write multiple registers'" do
47
+ @slave.should_receive(:query).with("\x10\x0\x1\x0\x3\x6\x0\xa\x1\x2\xf\xf").and_return("\x1\x0\x3")
48
+ @slave.write_multiple_registers(0x1,[0x000a,0x0102, 0xf0f]).should == @slave
49
+ end
50
+
51
+ it "should support function 'mask write register'" do
52
+ @slave.should_receive(:query).with("\x16\x0\x4\x0\xf2\x0\2").and_return("\x4\x0\xf2\x0\x2")
53
+ @slave.mask_write_register(0x4, 0xf2, 0x2).should == @slave
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ # -*- coding: ascii
2
+ require "rmodbus"
3
+
4
+ class RaiseResponseMismatch
5
+ def initialize(message, request, response)
6
+ @expected_message, @expected_request, @expected_response = message, request, response
7
+ end
8
+
9
+ def matches?(given_block)
10
+ begin
11
+ given_block.call
12
+ rescue ModBus::Errors::ResponseMismatch => e
13
+ @actual_message = e.message
14
+ @actual_request = e.request
15
+ @actual_response = e.response
16
+
17
+ @with_expected_message = verify_message
18
+ @with_expected_request = @expected_request == @actual_request
19
+ @with_expected_response = @expected_response == @actual_response
20
+ end
21
+ @with_expected_message & @with_expected_request & @with_expected_response
22
+ end
23
+
24
+ def failure_message
25
+ unless @with_expected_message
26
+ return "Expected message '#{@expected_message}', got '#{@actual_message}'"
27
+ end
28
+
29
+ unless @with_expected_request
30
+ return "Expected request #{logging_bytes @expected_request}, got #{logging_bytes @actual_request}"
31
+ end
32
+
33
+ unless @with_expected_response
34
+ return "Expected response #{logging_bytes @expected_response}, got #{logging_bytes @actual_response}"
35
+ end
36
+ end
37
+
38
+ def verify_message
39
+ case @expected_message
40
+ when nil
41
+ true
42
+ when Regexp
43
+ @expected_message =~ @actual_message
44
+ else
45
+ @expected_message == @actual_message
46
+ end
47
+ end
48
+ end
49
+
50
+ module RaiseResponseMatcher
51
+ def raise_response_mismatch(message, request, response)
52
+ RaiseResponseMismatch.new(message, request, response)
53
+ end
54
+ end
@@ -0,0 +1,88 @@
1
+ # -*- coding: ascii
2
+ require 'rmodbus'
3
+
4
+ describe ModBus::TCPClient do
5
+ describe "method 'query'" do
6
+ before(:each) do
7
+ @uid = 1
8
+ @sock = double('Socket')
9
+ @adu = "\000\001\000\000\000\001\001"
10
+
11
+ Socket.should_receive(:tcp).with('127.0.0.1', 1502, nil, nil, hash_including(:connect_timeout)).and_return(@sock)
12
+ @sock.stub(:read).with(0).and_return('')
13
+ @cl = ModBus::TCPClient.new('127.0.0.1', 1502)
14
+ @slave = @cl.with_slave(@uid)
15
+ end
16
+
17
+ it 'should send valid MBAP Header' do
18
+ @adu[0,2] = @slave.transaction.next.to_word
19
+ @sock.should_receive(:write).with(@adu)
20
+ @sock.should_receive(:read).with(7).and_return(@adu)
21
+ @slave.query('').should == nil
22
+ end
23
+
24
+ it 'should not throw exception and white next packet if get other transaction' do
25
+ @adu[0,2] = @slave.transaction.next.to_word
26
+ @sock.should_receive(:write).with(@adu)
27
+ @sock.should_receive(:read).with(7).and_return("\000\002\000\000\000\001" + @uid.chr)
28
+ @sock.should_receive(:read).with(7).and_return("\000\001\000\000\000\001" + @uid.chr)
29
+
30
+ expect{ @slave.query('') }.to_not raise_error
31
+ end
32
+
33
+ it 'should throw timeout exception if do not get own transaction' do
34
+ @slave.read_retries = 2
35
+ @adu[0,2] = @slave.transaction.next.to_word
36
+ @sock.should_receive(:write).at_least(1).times.with(/\.*/)
37
+ @sock.should_receive(:read).at_least(1).times.with(7).and_return("\000\x3\000\000\000\001" + @uid.chr)
38
+
39
+ expect{ @slave.query('') }.to raise_error(ModBus::Errors::ModBusTimeout, "Timed out during read attempt")
40
+ end
41
+
42
+
43
+ it 'should return only data from PDU' do
44
+ request = "\x3\x0\x6b\x0\x3"
45
+ response = "\x3\x6\x2\x2b\x0\x0\x0\x64"
46
+ @adu = @slave.transaction.next.to_word + "\x0\x0\x0\x9" + @uid.chr + request
47
+ @sock.should_receive(:write).with(@adu[0,4] + "\0\6" + @uid.chr + request)
48
+ @sock.should_receive(:read).with(7).and_return(@adu[0,7])
49
+ @sock.should_receive(:read).with(8).and_return(response)
50
+
51
+ @slave.query(request).should == response[2..-1]
52
+ end
53
+
54
+ it 'should sugar connect method' do
55
+ ipaddr, port = '127.0.0.1', 502
56
+ Socket.should_receive(:tcp).with(ipaddr, port, nil, nil, hash_including(:connect_timeout)).and_return(@sock)
57
+ @sock.should_receive(:closed?).and_return(false)
58
+ @sock.should_receive(:close)
59
+ ModBus::TCPClient.connect(ipaddr, port) do |cl|
60
+ cl.ipaddr.should == ipaddr
61
+ cl.port.should == port
62
+ end
63
+ end
64
+
65
+ it 'should have closed? method' do
66
+ @sock.should_receive(:closed?).and_return(false)
67
+ @cl.closed?.should == false
68
+
69
+ @sock.should_receive(:closed?).and_return(false)
70
+ @sock.should_receive(:close)
71
+
72
+ @cl.close
73
+
74
+ @sock.should_receive(:closed?).and_return(true)
75
+ @cl.closed?.should == true
76
+ end
77
+
78
+ it 'should give slave object in block' do
79
+ @cl.with_slave(1) do |slave|
80
+ slave.uid = 1
81
+ end
82
+ end
83
+ end
84
+
85
+ it "should tune connection timeout" do
86
+ lambda { ModBus::TCPClient.new('81.123.231.11', 1999, :connect_timeout => 0.001) }.should raise_error(ModBus::Errors::ModBusTimeout)
87
+ end
88
+ end
@@ -0,0 +1,158 @@
1
+ # -*- coding: ascii
2
+ require "rmodbus"
3
+
4
+ describe ModBus::TCPServer do
5
+ before :all do
6
+ unit_ids = (1..247).to_a.shuffle
7
+ valid_unit_id = unit_ids.first
8
+ @invalid_unit_id = unit_ids.last
9
+ @port = 8502
10
+ begin
11
+ @server = ModBus::TCPServer.new(@port)
12
+ @server_slave = @server.with_slave(valid_unit_id)
13
+ @server_slave.coils = [1,0,1,1]
14
+ @server_slave.discrete_inputs = [1,1,0,0]
15
+ @server_slave.holding_registers = [1,2,3,4]
16
+ @server_slave.input_registers = [1,2,3,4]
17
+ @server.start
18
+ rescue Errno::EADDRINUSE
19
+ @port += 1
20
+ retry
21
+ end
22
+ @cl = ModBus::TCPClient.new('127.0.0.1', @port)
23
+ @cl.read_retries = 1
24
+ @slave = @cl.with_slave(valid_unit_id)
25
+ end
26
+
27
+ it "should succeed if UID is broadcast" do
28
+ @cl.with_slave(0).write_coil(1,1)
29
+ # have to wait for the server to process it
30
+ sleep 1
31
+ @server_slave.coils[1].should == 1
32
+ end
33
+
34
+ it "should fail if UID is mismatched" do
35
+ lambda { @cl.with_slave(@invalid_unit_id).read_coils(1,3) }.should raise_exception(
36
+ ModBus::Errors::ModBusTimeout
37
+ )
38
+ end
39
+
40
+ it "should send exception if function not supported" do
41
+ lambda { @slave.query('0x43') }.should raise_exception(
42
+ ModBus::Errors::IllegalFunction,
43
+ "The function code received in the query is not an allowable action for the server"
44
+ )
45
+ end
46
+
47
+ it "should send exception if quanity of registers are more than 0x7d" do
48
+ lambda { @slave.read_holding_registers(0, 0x7e) }.should raise_exception(
49
+ ModBus::Errors::IllegalDataValue,
50
+ "A value contained in the query data field is not an allowable value for server"
51
+ )
52
+ end
53
+
54
+ it "shouldn't send exception if quanity of coils are more than 0x7d0" do
55
+ lambda { @slave.read_coils(0, 0x7d1) }.should raise_exception(
56
+ ModBus::Errors::IllegalDataValue,
57
+ "A value contained in the query data field is not an allowable value for server"
58
+ )
59
+ end
60
+
61
+ it "should send exception if addr not valid" do
62
+ lambda { @slave.read_coils(2, 8) }.should raise_exception(
63
+ ModBus::Errors::IllegalDataAddress,
64
+ "The data address received in the query is not an allowable address for the server"
65
+ )
66
+ end
67
+
68
+ it "should send exception if function not supported" do
69
+ lambda { @slave.query('0x43') }.should raise_exception(
70
+ ModBus::Errors::IllegalFunction,
71
+ "The function code received in the query is not an allowable action for the server"
72
+ )
73
+ end
74
+
75
+ it "should calc a many requests" do
76
+ @slave.read_coils(1,2)
77
+ @slave.write_multiple_registers(0,[9,9,9,])
78
+ @slave.read_holding_registers(0,3).should == [9,9,9]
79
+ end
80
+
81
+ it "should supported function 'read coils'" do
82
+ @slave.read_coils(0,3).should == @server_slave.coils[0,3]
83
+ end
84
+
85
+ it "should supported function 'read coils' with more than 125 in one request" do
86
+ @server_slave.coils = Array.new( 1900, 1 )
87
+ @slave.read_coils(0,1900).should == @server_slave.coils[0,1900]
88
+ end
89
+
90
+ it "should supported function 'read discrete inputs'" do
91
+ @slave.read_discrete_inputs(1,3).should == @server_slave.discrete_inputs[1,3]
92
+ end
93
+
94
+ it "should supported function 'read holding registers'" do
95
+ @slave.read_holding_registers(0,3).should == @server_slave.holding_registers[0,3]
96
+ end
97
+
98
+ it "should supported function 'read input registers'" do
99
+ @slave.read_input_registers(2,2).should == @server_slave.input_registers[2,2]
100
+ end
101
+
102
+ it "should supported function 'write single coil'" do
103
+ @server_slave.coils[3] = 0
104
+ @slave.write_single_coil(3,1)
105
+ @server_slave.coils[3].should == 1
106
+ end
107
+
108
+ it "should supported function 'write single register'" do
109
+ @server_slave.holding_registers[3] = 25
110
+ @slave.write_single_register(3,35)
111
+ @server_slave.holding_registers[3].should == 35
112
+ end
113
+
114
+ it "should supported function 'write multiple coils'" do
115
+ @server_slave.coils = [1,1,1,0, 0,0,0,0, 0,0,0,0, 0,1,1,1]
116
+ @slave.write_multiple_coils(3, [1, 0,1,0,1, 0,1,0,1])
117
+ @server_slave.coils.should == [1,1,1,1, 0,1,0,1, 0,1,0,1, 0,1,1,1]
118
+ end
119
+
120
+ it "should supported function 'write multiple registers'" do
121
+ @server_slave.holding_registers = [1,2,3,4,5,6,7,8,9]
122
+ @slave.write_multiple_registers(3,[1,2,3,4,5])
123
+ @server_slave.holding_registers.should == [1,2,3,1,2,3,4,5,9]
124
+ end
125
+
126
+ it "should support function 'mask_write_register'" do
127
+ @server_slave.holding_registers = [0x12]
128
+ @slave.mask_write_register(0, 0xf2, 0x25)
129
+ @server_slave.holding_registers.should == [0x17]
130
+ end
131
+
132
+ it "should support function 'read_write_multiple_registers'" do
133
+ @server_slave.holding_registers = [1,2,3,4,5,6,7,8,9]
134
+ @slave.read_write_multiple_registers(0, 5, 4, [3,2,1]).should == [1,2,3,4,3]
135
+ @server_slave.holding_registers.should == [1,2,3,4,3,2,1,8,9]
136
+ end
137
+
138
+ it "should have options :host" do
139
+ host = '192.168.0.1'
140
+ srv = ModBus::TCPServer.new(1010, :host => '192.168.0.1')
141
+ srv.host.should eql(host)
142
+ end
143
+
144
+ it "should have options :max_connection" do
145
+ max_conn = 5
146
+ srv = ModBus::TCPServer.new(1010, :max_connection => 5)
147
+ srv.maxConnections.should eql(max_conn)
148
+ end
149
+
150
+ after :all do
151
+ @cl.close unless @cl.closed?
152
+ @server.stop unless @server.stopped?
153
+ while GServer.in_service?(@port)
154
+ sleep(0.01)
155
+ end
156
+ @server.stop
157
+ end
158
+ end