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,55 @@
1
+ module ModBus
2
+ # TCP slave implementation
3
+ # @example
4
+ # TCP.connect('127.0.0.1', 10002) do |cl|
5
+ # cl.with_slave(uid) do |slave|
6
+ # slave.holding_registers[0..100]
7
+ # end
8
+ # end
9
+ #
10
+ # @see TCP#open_tcp_connection
11
+ # @see Client#with_slave
12
+ # @see Slave
13
+ class TCPSlave < Client::Slave
14
+ attr_reader :transaction
15
+
16
+ # @see Slave::initialize
17
+ def initialize(uid, io)
18
+ @transaction = 0
19
+ super(uid, io)
20
+ end
21
+
22
+ private
23
+ # overide method for RTU over TCP implamentaion
24
+ # @see Slave#query
25
+ def send_pdu(pdu)
26
+ @transaction = 0 if @transaction.next > 65535
27
+ @transaction += 1
28
+ msg = @transaction.to_word + "\0\0" + (pdu.size + 1).to_word + @uid.chr + pdu
29
+ @io.write msg
30
+
31
+ log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
32
+ end
33
+
34
+ # overide method for RTU over TCP implamentaion
35
+ # @see Slave#query
36
+ def read_pdu
37
+ loop do
38
+ header = @io.read(7)
39
+ if header
40
+ trn = header[0,2].unpack('n')[0]
41
+ len = header[4,2].unpack('n')[0]
42
+ msg = @io.read(len-1)
43
+
44
+ log "Rx (#{(header + msg).size} bytes): " + logging_bytes(header + msg)
45
+
46
+ if trn == @transaction
47
+ return msg
48
+ else
49
+ log "Transaction number mismatch. A packet is ignored."
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module ModBus
2
+ VERSION = '2.0.0'
3
+ end
@@ -0,0 +1,88 @@
1
+ # -*- coding: ascii
2
+ require 'rmodbus'
3
+
4
+ describe ModBus::Client do
5
+ before do
6
+ @cl = ModBus::Client.new
7
+ end
8
+
9
+ it "should give object provider for slave" do
10
+ slave = @cl.with_slave(1)
11
+ slave.uid.should eq(1)
12
+ end
13
+
14
+ it "should give object provider for slave in block" do
15
+ @cl.with_slave(1) do |slave|
16
+ slave.uid.should eq(1)
17
+ end
18
+ end
19
+
20
+ it "should connect with TCP server" do
21
+ ModBus::Client.connect do |cl|
22
+ cl.should be_instance_of(ModBus::Client)
23
+ end
24
+ end
25
+
26
+ it ":new alias :connect" do
27
+ ModBus::Client.new do |cl|
28
+ cl.should be_instance_of(ModBus::Client)
29
+ end
30
+ end
31
+
32
+ it "should close the connection when an exception is raised in the given block" do
33
+ expect {
34
+ ModBus::Client.new do |client|
35
+ client.should_receive(:close)
36
+ raise
37
+ end
38
+ }.to raise_error
39
+ end
40
+
41
+ it 'should common for all slaves :debug flag' do
42
+ @cl.debug = true
43
+ @cl.with_slave(1) do |slave_1|
44
+ slave_1.debug.should be_truthy
45
+ end
46
+ @cl.with_slave(2) do |slave_2|
47
+ slave_2.debug = false
48
+ slave_2.debug.should be_falsey
49
+ end
50
+ end
51
+
52
+ it 'should common for all slaves :raise_exception_on_mismatch flag' do
53
+ @cl.raise_exception_on_mismatch = true
54
+ @cl.with_slave(1) do |slave_1|
55
+ slave_1.raise_exception_on_mismatch.should be_truthy
56
+ end
57
+
58
+ @cl.with_slave(2) do |slave_2|
59
+ slave_2.raise_exception_on_mismatch = false
60
+ slave_2.raise_exception_on_mismatch.should be_falsey
61
+ end
62
+ end
63
+
64
+ it 'should common for all slaves :read_retries options' do
65
+ @cl.read_retries = 5
66
+ @cl.with_slave(1) do |slave_1|
67
+ slave_1.read_retries.should eql(5)
68
+ end
69
+
70
+ @cl.with_slave(2) do |slave_2|
71
+ slave_2.read_retries = 15
72
+ slave_2.read_retries.should eql(15)
73
+ end
74
+ end
75
+
76
+ it 'should common for all slaves :read_retry_timeout options' do
77
+ @cl.read_retry_timeout = 5
78
+ @cl.with_slave(1) do |slave_1|
79
+ slave_1.read_retry_timeout.should eql(5)
80
+ end
81
+
82
+ @cl.with_slave(2) do |slave_2|
83
+ slave_2.read_retry_timeout = 15
84
+ slave_2.read_retry_timeout.should eql(15)
85
+ end
86
+ end
87
+
88
+ end
@@ -0,0 +1,120 @@
1
+ # -*- coding: ascii
2
+ require 'rmodbus'
3
+
4
+ describe ModBus::TCPClient do
5
+ before(:all) do
6
+ @srv = ModBus::TCPServer.new(1502)
7
+ srv_slave = @srv.with_slave(1)
8
+ srv_slave.coils = [0] * 8
9
+ srv_slave.discrete_inputs = [0] * 8
10
+ srv_slave.holding_registers = [0] * 8
11
+ srv_slave.input_registers = [0] * 8
12
+ @srv.start
13
+
14
+ @cl = ModBus::TCPClient.new('127.0.0.1', 1502)
15
+ @slave = @cl.with_slave(1)
16
+ end
17
+
18
+ it "should raise ProxException" do
19
+ lambda { @slave.holding_registers[0..2] = [0,0] }.should raise_error(ModBus::Errors::ProxyException)
20
+ end
21
+
22
+ # Read coil status
23
+ it "should read coil status" do
24
+ @slave.read_coils(0, 4).should == [0] * 4
25
+ end
26
+
27
+ it "should raise exception if illegal data address" do
28
+ lambda { @slave.read_coils(501, 34) }.should raise_error(ModBus::Errors::IllegalDataAddress)
29
+ end
30
+
31
+ it "should raise exception if too many data" do
32
+ lambda { @slave.read_coils(0, 0x07D1) }.should raise_error(ModBus::Errors::IllegalDataValue)
33
+ end
34
+
35
+ # Read input status
36
+ it "should read discrete inputs" do
37
+ @slave.read_discrete_inputs(0, 4).should == [0] * 4
38
+ end
39
+
40
+ it "should raise exception if illegal data address" do
41
+ lambda { @slave.read_discrete_inputs(50, 23) }.should raise_error(ModBus::Errors::IllegalDataAddress)
42
+ end
43
+
44
+ it "should raise exception if too many data" do
45
+ lambda { @slave.read_discrete_inputs(0, 0x07D1) }.should raise_error(ModBus::Errors::IllegalDataValue)
46
+ end
47
+
48
+ # Read holding registers
49
+ it "should read discrete inputs" do
50
+ @slave.read_holding_registers(0, 4).should == [0, 0, 0, 0]
51
+ end
52
+
53
+ it "should raise exception if illegal data address" do
54
+ lambda { @slave.read_holding_registers(402, 99) }.should raise_error(ModBus::Errors::IllegalDataAddress)
55
+ end
56
+
57
+
58
+ it "should raise exception if too many data" do
59
+ lambda { @slave.read_holding_registers(0, 0x007E) }.should raise_error(ModBus::Errors::IllegalDataValue)
60
+ end
61
+
62
+ # Read input registers
63
+ it "should read discrete inputs" do
64
+ @slave.read_input_registers(0, 4).should == [0, 0, 0, 0]
65
+ end
66
+
67
+ it "should raise exception if illegal data address" do
68
+ lambda { @slave.read_input_registers(402, 9) }.should raise_error(ModBus::Errors::IllegalDataAddress)
69
+ end
70
+
71
+ it "should raise exception if too many data" do
72
+ lambda { @slave.read_input_registers(0, 0x007E) }.should raise_error(ModBus::Errors::IllegalDataValue)
73
+ end
74
+
75
+ # Force single coil
76
+ it "should force single coil" do
77
+ @slave.write_single_coil(4, 1).should == @slave
78
+ @slave.read_coils(4, 4).should == [1, 0, 0, 0]
79
+ end
80
+
81
+ it "should raise exception if illegal data address" do
82
+ lambda { @slave.write_single_coil(501, true) }.should raise_error(ModBus::Errors::IllegalDataAddress)
83
+ end
84
+
85
+ # Preset single register
86
+ it "should preset single register" do
87
+ @slave.write_single_register(4, 0x0AA0).should == @slave
88
+ @slave.read_holding_registers(4, 1).should == [0x0AA0]
89
+ end
90
+
91
+ it "should raise exception if illegal data address" do
92
+ lambda { @slave.write_single_register(501, 0x0AA0) }.should raise_error(ModBus::Errors::IllegalDataAddress)
93
+ end
94
+
95
+ # Force multiple coils
96
+ it "should force multiple coils" do
97
+ @slave.write_multiple_coils(4, [0,1,0,1]).should == @slave
98
+ @slave.read_coils(3, 5).should == [0,0,1,0,1]
99
+ end
100
+
101
+ it "should raise exception if illegal data address" do
102
+ lambda { @slave.write_multiple_coils(501, [1,0]) }.should raise_error(ModBus::Errors::IllegalDataAddress)
103
+ end
104
+
105
+ # Preset multiple registers
106
+ it "should preset multiple registers" do
107
+ @slave.write_multiple_registers(4, [1, 2, 3, 0xAACC]).should == @slave
108
+ @slave.read_holding_registers(3, 5).should == [0, 1, 2, 3, 0xAACC]
109
+ end
110
+
111
+ it "should raise exception if illegal data address" do
112
+ lambda { @slave.write_multiple_registers(501, [1, 2]) }.should raise_error(ModBus::Errors::IllegalDataAddress)
113
+ end
114
+
115
+ after(:all) do
116
+ @cl.close unless @cl.closed?
117
+ @srv.stop
118
+ end
119
+
120
+ end
data/spec/ext_spec.rb ADDED
@@ -0,0 +1,52 @@
1
+ # -*- coding: ascii
2
+ require 'rmodbus'
3
+
4
+ describe Array do
5
+ before do
6
+ @arr = [1,0,1,1, 0,0,1,1, 1,1,0,1, 0,1,1,0, 1,0,1]
7
+ @test = [0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0]
8
+ end
9
+
10
+ it "should return string reprisent 16bit" do
11
+ @arr.pack_to_word.should == "\xcd\x6b\x5"
12
+ end
13
+
14
+ it "fixed bug for divisible 8 data " do
15
+ ([0] * 8).pack_to_word.should == "\x00"
16
+ end
17
+
18
+ it "should unpack to @test" do
19
+ "test".unpack_bits == @test
20
+ end
21
+
22
+ it "should turn an array into 32b ints" do
23
+ [20342, 17344].to_32i.should == [1136676726]
24
+ [20342, 17344, 20342, 17344].to_32i.size.should == 2
25
+ end
26
+
27
+ it "should turn an array into 32b floats big endian" do
28
+ [20342, 17344].to_32f[0].should be_within(0.1).of(384.620788574219)
29
+ [20342, 17344, 20342, 17344].to_32f.size.should == 2
30
+ end
31
+
32
+ it "should turn a an array into 32b floats (little endian)" do
33
+ [17344, 20342].to_32f_le[0].should be_within(0.1).of(384.620788574219)
34
+ [17344, 20342, 17344, 20342].to_32f_le.size.should == 2
35
+ end
36
+
37
+ it "should turn an array from 32b ints into 16b ints, big endian" do
38
+ [1136676726].from_32i.should == [20342, 17344]
39
+ [1136676726, 1136676725].from_32i.should == [20342, 17344, 20341, 17344]
40
+ end
41
+
42
+ it "should turn an array from 32b floats into 16b ints, big endian" do
43
+ [384.620788].from_32f.should == [20342, 17344]
44
+ [384.620788, 384.620788].from_32f.should == [20342, 17344, 20342, 17344]
45
+ end
46
+
47
+ it "should raise exception if uneven number of elements" do
48
+ lambda { [20342, 17344, 123].to_32f }.should raise_error(StandardError)
49
+ lambda { [20342, 17344, 123].to_32i }.should raise_error(StandardError)
50
+ end
51
+ end
52
+
@@ -0,0 +1,89 @@
1
+ # -*- coding: ascii
2
+ require 'rmodbus'
3
+
4
+ describe ModBus::TCPClient do
5
+ before(:each) do
6
+ @uid = 1
7
+ @sock = double('Socket')
8
+ @adu = "\000\001\000\000\000\001\001"
9
+
10
+ Socket.should_receive(:tcp).with('127.0.0.1', 1502, nil, nil, hash_including(:connect_timeout)).and_return(@sock)
11
+ @sock.stub(:read).with(0).and_return('')
12
+
13
+ @slave = ModBus::TCPClient.new('127.0.0.1', 1502).with_slave(@uid)
14
+ @slave.debug = true
15
+ end
16
+
17
+ it 'should log rec\send bytes' do
18
+ request, response = "\x3\x0\x6b\x0\x3", "\x3\x6\x2\x2b\x0\x0\x0\x64"
19
+ mock_query(request,response)
20
+ $stdout.should_receive(:puts).with("Tx (12 bytes): [00][01][00][00][00][06][01][03][00][6b][00][03]")
21
+ $stdout.should_receive(:puts).with("Rx (15 bytes): [00][01][00][00][00][09][01][03][06][02][2b][00][00][00][64]")
22
+ @slave.query(request)
23
+ end
24
+
25
+ it "should don't logging if debug disable" do
26
+ @slave.debug = false
27
+ request, response = "\x3\x0\x6b\x0\x3", "\x3\x6\x2\x2b\x0\x0\x0\x64"
28
+ mock_query(request,response)
29
+ @slave.query(request)
30
+ end
31
+
32
+ it "should log warn message if transaction mismatch" do
33
+ @adu[0,2] = @slave.transaction.next.to_word
34
+ @sock.should_receive(:write).with(@adu)
35
+ @sock.should_receive(:read).with(7).and_return("\000\002\000\000\000\001" + @uid.chr)
36
+ @sock.should_receive(:read).with(7).and_return("\000\001\000\000\000\001" + @uid.chr)
37
+
38
+ $stdout.should_receive(:puts).with("Tx (7 bytes): [00][01][00][00][00][01][01]")
39
+ $stdout.should_receive(:puts).with("Rx (7 bytes): [00][02][00][00][00][01][01]")
40
+ $stdout.should_receive(:puts).with("Transaction number mismatch. A packet is ignored.")
41
+ $stdout.should_receive(:puts).with("Rx (7 bytes): [00][01][00][00][00][01][01]")
42
+
43
+ @slave.query('')
44
+ end
45
+
46
+ def mock_query(request, response)
47
+ @adu = @slave.transaction.next.to_word + "\x0\x0\x0\x9" + @uid.chr + request
48
+ @sock.should_receive(:write).with(@adu[0,4] + "\0\6" + @uid.chr + request)
49
+ @sock.should_receive(:read).with(7).and_return(@adu[0,7])
50
+ @sock.should_receive(:read).with(8).and_return(response)
51
+ end
52
+ end
53
+
54
+ begin
55
+ require "serialport"
56
+ describe ModBus::RTUClient do
57
+ before do
58
+ @sp = double('Serial port')
59
+
60
+ SerialPort.should_receive(:new).with("/dev/port1", 9600, 7, 2, SerialPort::ODD).and_return(@sp)
61
+ SerialPort.stub(:public_method_defined?).with(:flush_input).and_return(true)
62
+
63
+ @sp.stub(:class).and_return(SerialPort)
64
+ @sp.should_receive(:flow_control=).with(SerialPort::NONE)
65
+ @sp.stub(:read_timeout=)
66
+ @sp.stub(:flush_input)
67
+
68
+ @slave = ModBus::RTUClient.new("/dev/port1", 9600, :data_bits => 7, :stop_bits => 2, :parity => SerialPort::ODD).with_slave(1)
69
+ @slave.read_retries = 0
70
+
71
+ end
72
+
73
+ it 'should log rec\send bytes' do
74
+ request = "\x3\x0\x1\x0\x1"
75
+ @sp.should_receive(:write).with("\1#{request}\xd5\xca")
76
+ @sp.should_receive(:flush_input) # Clean a garbage
77
+ @sp.should_receive(:read).with(2).and_return("\x1\x3")
78
+ @sp.should_receive(:read).with(1).and_return("\x2")
79
+ @sp.should_receive(:read).with(4).and_return("\xff\xff\xb9\xf4")
80
+
81
+ @slave.debug = true
82
+ $stdout.should_receive(:puts).with("Tx (8 bytes): [01][03][00][01][00][01][d5][ca]")
83
+ $stdout.should_receive(:puts).with("Rx (7 bytes): [01][03][02][ff][ff][b9][f4]")
84
+
85
+ @slave.query(request).should == "\xff\xff"
86
+ end
87
+ end
88
+ rescue LoadError
89
+ end
@@ -0,0 +1,74 @@
1
+ # -*- coding: ascii
2
+ require 'rmodbus'
3
+
4
+ describe Array do
5
+ before do
6
+ @slave = double('ModBus Slave')
7
+ @coil_proxy = ModBus::ReadWriteProxy.new(@slave, :coil)
8
+ @discrete_input_proxy = ModBus::ReadOnlyProxy.new(@slave, :discrete_input)
9
+ @holding_register_proxy = ModBus::ReadWriteProxy.new(@slave, :holding_register)
10
+ @input_register_proxy = ModBus::ReadOnlyProxy.new(@slave, :input_register)
11
+ end
12
+
13
+ # Handle all of the coil methods
14
+ it "should call read_coil" do
15
+ @slave.should_receive(:read_coil).with(0, 1)
16
+ @coil_proxy[0]
17
+ end
18
+ it "should call read_coils" do
19
+ @slave.should_receive(:read_coils).with(0, 2)
20
+ @coil_proxy[0..1]
21
+ end
22
+ it "should call write_coil" do
23
+ @slave.should_receive(:write_coil).with(0, 1)
24
+ @coil_proxy[0] = 1
25
+ end
26
+ it "should call write_coils" do
27
+ @slave.should_receive(:write_coils).with(0, [0, 0])
28
+ @coil_proxy[0..1] = [0, 0]
29
+ end
30
+
31
+
32
+ # Discrete input tests
33
+ it "should call read_discrete_input" do
34
+ @slave.should_receive(:read_discrete_input).with(0, 1)
35
+ @discrete_input_proxy[0]
36
+ end
37
+
38
+ it "should call read_discrete_inputs" do
39
+ @slave.should_receive(:read_discrete_inputs).with(0, 2)
40
+ @discrete_input_proxy[0..1]
41
+ end
42
+
43
+
44
+ # Holding Register Tess
45
+ it "should call read_holding_register" do
46
+ @slave.should_receive(:read_holding_register).with(0, 1)
47
+ @holding_register_proxy[0]
48
+ end
49
+ it "should call read_holding_registers" do
50
+ @slave.should_receive(:read_holding_registers).with(0, 2)
51
+ @holding_register_proxy[0..1]
52
+ end
53
+ it "should call write_holding_register" do
54
+ @slave.should_receive(:write_holding_register).with(0, 1)
55
+ @holding_register_proxy[0] = 1
56
+ end
57
+ it "should call write_holding_registers" do
58
+ @slave.should_receive(:write_holding_registers).with(0, [0, 0])
59
+ @holding_register_proxy[0..1] = [0, 0]
60
+ end
61
+
62
+
63
+ # Input Register Tests
64
+ it "should call read_discrete_input" do
65
+ @slave.should_receive(:read_input_register).with(0, 1)
66
+ @input_register_proxy[0]
67
+ end
68
+
69
+ it "should call read_discrete_inputs" do
70
+ @slave.should_receive(:read_input_registers).with(0, 2)
71
+ @input_register_proxy[0..1]
72
+ end
73
+ end
74
+