rmodbus-ccutrer 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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