sanderjd-rmodbus 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,160 @@
1
+ # RModBus - free implementation of ModBus protocol on Ruby.
2
+ #
3
+ # Copyright (C) 2008 Timin Aleksey
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ require 'rmodbus/exceptions'
15
+ require 'gserver'
16
+
17
+
18
+ module ModBus
19
+
20
+ class TCPServer < GServer
21
+
22
+ attr_accessor :coils, :discret_inputs, :holding_registers, :input_registers
23
+
24
+ @@funcs = [1,2,3,4,5,6,15,16]
25
+
26
+ def initialize(port = 502, uid = 1)
27
+ @coils = []
28
+ @discret_inputs = []
29
+ @holding_registers =[]
30
+ @input_registers = []
31
+ @uid = uid
32
+ super(port)
33
+ end
34
+
35
+ def serve(io)
36
+ req = io.read(7)
37
+ if req[2,2] != "\x00\x00" or req[6].to_i != @uid
38
+ io.close
39
+ return
40
+ end
41
+
42
+ tr = req[0,2]
43
+ len = req[4,2].to_int16
44
+ req = io.read(len - 1)
45
+ func = req[0].to_i
46
+
47
+ unless @@funcs.include?(func)
48
+ param = { :err => 1 }
49
+ end
50
+
51
+ case func
52
+ when 1
53
+ param = parse_read_func(req, @coils)
54
+ if param[:err] == 0
55
+ val = @coils[param[:addr],param[:quant]].bits_to_bytes
56
+ res = func.chr + val.size.chr + val
57
+ end
58
+ when 2
59
+ param = parse_read_func(req, @discret_inputs)
60
+ if param[:err] == 0
61
+ val = @discret_inputs[param[:addr],param[:quant]].bits_to_bytes
62
+ res = func.chr + val.size.chr + val
63
+ end
64
+ when 3
65
+ param = parse_read_func(req, @holding_registers)
66
+ if param[:err] == 0
67
+ res = func.chr + (param[:quant] * 2).chr + @holding_registers[param[:addr],param[:quant]].to_ints16
68
+ end
69
+ when 4
70
+ param = parse_read_func(req, @input_registers)
71
+ if param[:err] == 0
72
+ res = func.chr + (param[:quant] * 2).chr + @input_registers[param[:addr],param[:quant]].to_ints16
73
+ end
74
+ when 5
75
+ param = parse_write_coil_func(req)
76
+ if param[:err] == 0
77
+ @coils[param[:addr]] = param[:val]
78
+ res = func.chr + req
79
+ end
80
+ when 6
81
+ param = parse_write_register_func(req)
82
+ if param[:err] == 0
83
+ @holding_registers[param[:addr]] = param[:val]
84
+ res = func.chr + req
85
+ end
86
+ when 15
87
+ param = parse_write_multiple_coils_func(req)
88
+ if param[:err] == 0
89
+ @coils[param[:addr],param[:quant]] = param[:val][0,param[:quant]]
90
+ res = func.chr + req
91
+ end
92
+ when 16
93
+ param = parse_write_multiple_registers_func(req)
94
+ if param[:err] == 0
95
+ @holding_registers[param[:addr],param[:quant]] = param[:val][0,param[:quant]]
96
+ res = func.chr + req
97
+ end
98
+ end
99
+ if param[:err] == 0
100
+ io.write tr + "\0\0" + (res.size + 1).to_bytes + @uid.chr + res
101
+ else
102
+ io.write tr + "\0\0\0\3" + @uid.chr + (func | 0x80).chr + param[:err].chr
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def parse_read_func(req, field)
109
+ quant = req[3,2].to_int16
110
+
111
+ return { :err => 3} unless quant <= 0x7d
112
+
113
+ addr = req[1,2].to_int16
114
+ return { :err => 2 } unless addr + quant <= field.size
115
+
116
+ return { :err => 0, :quant => quant, :addr => addr }
117
+ end
118
+
119
+ def parse_write_coil_func(req)
120
+ addr = req[1,2].to_int16
121
+ return { :err => 2 } unless addr <= @coils.size
122
+
123
+ val = req[3,2].to_int16
124
+ return { :err => 3 } unless val == 0 or val == 0xff00
125
+
126
+ val = 1 if val == 0xff00
127
+ return { :err => 0, :addr => addr, :val => val }
128
+ end
129
+
130
+ def parse_write_register_func(req)
131
+ addr = req[1,2].to_int16
132
+ return { :err => 2 } unless addr <= @coils.size
133
+
134
+ val = req[3,2].to_int16
135
+
136
+ return { :err => 0, :addr => addr, :val => val }
137
+ end
138
+
139
+ def parse_write_multiple_coils_func(req)
140
+ param = parse_read_func(req, @coils)
141
+
142
+ if param[:err] == 0
143
+ param = {:err => 0, :addr => param[:addr], :quant => param[:quant], :val => req[6,param[:quant]].to_array_bit }
144
+ end
145
+ param
146
+ end
147
+
148
+ def parse_write_multiple_registers_func(req)
149
+ param = parse_read_func(req, @holding_registers)
150
+
151
+ if param[:err] == 0
152
+ # Each val is 2 bytes, so multiply by two to get number of bytes
153
+ bytes_for_vals = param[:quant] * 2
154
+ param = {:err => 0, :addr => param[:addr], :quant => param[:quant], :val => req[6,bytes_for_vals].to_array_int16 }
155
+ end
156
+ param
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,57 @@
1
+ require 'rmodbus/client'
2
+
3
+ include ModBus
4
+
5
+ describe Client do
6
+
7
+ before do
8
+ @cl_mb = Client.new
9
+ @cl_mb.stub!(:query).and_return('')
10
+ end
11
+
12
+ it "should support function 'read coils'" do
13
+ @cl_mb.should_receive(:query).with("\x1\x0\x13\x0\x13").and_return("\xcd\x6b\x5")
14
+ @cl_mb.read_coils(0x13,0x13).should == [1,0,1,1, 0,0,1,1, 1,1,0,1, 0,1,1,0, 1,0,1]
15
+ end
16
+
17
+ it "should support function 'read discrete inputs'" do
18
+ @cl_mb.should_receive(:query).with("\x2\x0\xc4\x0\x16").and_return("\xac\xdb\x35")
19
+ @cl_mb.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]
20
+ end
21
+
22
+ it "should support function 'read holding registers'" do
23
+ @cl_mb.should_receive(:query).with("\x3\x0\x6b\x0\x3").and_return("\x2\x2b\x0\x0\x0\x64")
24
+ @cl_mb.read_holding_registers(0x6b,0x3).should == [0x022b, 0x0000, 0x0064]
25
+ end
26
+
27
+ it "should support function 'read input registers'" do
28
+ @cl_mb.should_receive(:query).with("\x4\x0\x8\x0\x1").and_return("\x0\xa")
29
+ @cl_mb.read_input_registers(0x8,0x1).should == [0x000a]
30
+ end
31
+
32
+ it "should support function 'write single coil'" do
33
+ @cl_mb.should_receive(:query).with("\x5\x0\xac\xff\x0").and_return("\xac\xff\x00")
34
+ @cl_mb.write_single_coil(0xac,0x1).should == @cl_mb
35
+ end
36
+
37
+ it "should support function 'write single register'" do
38
+ @cl_mb.should_receive(:query).with("\x6\x0\x1\x0\x3").and_return("\x1\x0\x3")
39
+ @cl_mb.write_single_register(0x1,0x3).should == @cl_mb
40
+ end
41
+
42
+ it "should support function 'write multiple coils'" do
43
+ @cl_mb.should_receive(:query).with("\xf\x0\x13\x0\xa\x2\xcd\x1").and_return("\x13\x0\xa")
44
+ @cl_mb.write_multiple_coils(0x13,[1,0,1,1, 0,0,1,1, 1,0]).should == @cl_mb
45
+ end
46
+
47
+ it "should support function 'write multiple registers'" do
48
+ @cl_mb.should_receive(:query).with("\x10\x0\x1\x0\x3\x6\x0\xa\x1\x2\xf\xf").and_return("\x1\x0\x3")
49
+ @cl_mb.write_multiple_registers(0x1,[0x000a,0x0102, 0xf0f]).should == @cl_mb
50
+ end
51
+
52
+ it "should support function 'mask write register'" do
53
+ @cl_mb.should_receive(:query).with("\x16\x0\x4\x0\xf2\x0\2").and_return("\x4\x0\xf2\x0\x2")
54
+ @cl_mb.mask_write_register(0x4, 0xf2, 0x2).should == @cl_mb
55
+ end
56
+
57
+ end
@@ -0,0 +1,27 @@
1
+ require 'rmodbus'
2
+
3
+ describe Array do
4
+
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
+ end
8
+
9
+ it "should return string reprisent 16bit" do
10
+ @arr.bits_to_bytes.should == "\xcd\x6b\x5"
11
+ end
12
+
13
+ it "should return string reprisent 16ints" do
14
+ @arr = [1,2]
15
+ @arr.to_ints16 == "\x0\x1\x0\x2"
16
+ end
17
+
18
+ end
19
+
20
+ describe String do
21
+
22
+ it "should return array of int16" do
23
+ @str = "\x1\x2\x3\x4\x5\x6"
24
+ @str.to_array_int16.should == [0x102, 0x304, 0x506]
25
+ end
26
+
27
+ end
@@ -0,0 +1,13 @@
1
+ require 'rmodbus/rtu_client'
2
+
3
+ include ModBus
4
+
5
+ describe RTUClient do
6
+
7
+ before do
8
+ @mb_cl = RTUClient.new(1, 9600, UID)
9
+ end
10
+
11
+ # it "should
12
+
13
+ end
@@ -0,0 +1,48 @@
1
+ require 'rmodbus/tcp_client'
2
+
3
+ include ModBus
4
+
5
+ describe TCPClient, "method 'query'" do
6
+
7
+ UID = 1
8
+
9
+ before do
10
+ @sock = mock("Socket")
11
+ @adu = "\000\001\000\000\000\001\001"
12
+
13
+ TCPSocket.should_receive(:new).with('127.0.0.1', 1502).and_return(@sock)
14
+ @sock.stub!(:read).with(0).and_return('')
15
+
16
+ @mb_client = TCPClient.new('127.0.0.1', 1502)
17
+ end
18
+
19
+ it 'should send valid MBAP Header' do
20
+ @adu[0,2] = TCPClient.transaction.next.to_bytes
21
+ @sock.should_receive(:write).with(@adu)
22
+ @sock.should_receive(:read).with(7).and_return(@adu)
23
+ @mb_client.query('').should == nil
24
+ end
25
+
26
+ it 'should throw exception if get other transaction' do
27
+ @adu[0,2] = TCPClient.transaction.next.to_bytes
28
+ @sock.should_receive(:write).with(@adu)
29
+ @sock.should_receive(:read).with(7).and_return("\000\002\000\000\000\001" + UID.chr)
30
+ begin
31
+ @mb_client.query('').should == nil
32
+ rescue Exception => ex
33
+ ex.class.should == Errors::ModBusException
34
+ end
35
+ end
36
+
37
+ it 'should return only data from PDU' do
38
+ request = "\x3\x0\x6b\x0\x3"
39
+ response = "\x3\x6\x2\x2b\x0\x0\x0\x64"
40
+ @adu = TCPClient.transaction.next.to_bytes + "\x0\x0\x0\x9" + UID.chr + request
41
+ @sock.should_receive(:write).with(@adu[0,4] + "\0\6" + UID.chr + request)
42
+ @sock.should_receive(:read).with(7).and_return(@adu[0,7])
43
+ @sock.should_receive(:read).with(8).and_return(response)
44
+
45
+ @mb_client.query(request).should == response[2..-1]
46
+ end
47
+
48
+ end
@@ -0,0 +1,107 @@
1
+ require 'rmodbus'
2
+
3
+ describe TCPServer do
4
+
5
+ before do
6
+ @server = ModBus::TCPServer.new(8502,1)
7
+ @server.coils = [1,0,1,1]
8
+ @server.discret_inputs = [1,1,0,0]
9
+ @server.holding_registers = [1,2,3,4]
10
+ @server.input_registers = [1,2,3,4]
11
+ @server.start
12
+ @client = ModBus::TCPClient.new('127.0.0.1', 8502, 1)
13
+ end
14
+
15
+ it "should silent if UID has mismatched" do
16
+ @client.close
17
+ client = ModBus::TCPClient.new('127.0.0.1', 8502, 2)
18
+ begin
19
+ client.read_coils(1,3)
20
+ rescue ModBus::Errors::ModBusException => ex
21
+ ex.message.should == "Server did not respond"
22
+ end
23
+ end
24
+
25
+ it "should silent if protocol identifer has mismatched" do
26
+ socket = TCPSocket.new('127.0.0.1', 8502)
27
+ begin
28
+ socket.write "\0\0\1\0\0\6\1"
29
+ rescue ModBus::Errors::ModBusException => ex
30
+ ex.message.should == "Server did not respond"
31
+ rescue
32
+ # Fails if it is whiny about anything else
33
+ assert false
34
+ ensure
35
+ socket.close
36
+ end
37
+ end
38
+
39
+ it "should send exception if function not supported" do
40
+ begin
41
+ @client.query('0x43')
42
+ rescue ModBus::Errors::IllegalFunction => ex
43
+ ex.message.should == "The function code received in the query is not an allowable action for the server"
44
+ end
45
+ end
46
+
47
+ it "should send exception if quanity of out more 0x7d" do
48
+ begin
49
+ @client.read_coils(0, 0x7e)
50
+ rescue ModBus::Errors::IllegalDataValue => ex
51
+ ex.message.should == "A value contained in the query data field is not an allowable value for server"
52
+ end
53
+ end
54
+
55
+ it "should send exception if addr not valid" do
56
+ begin
57
+ @client.read_coils(2, 8)
58
+ rescue ModBus::Errors::IllegalDataAddress => ex
59
+ ex.message.should == "The data address received in the query is not an allowable address for the server"
60
+ end
61
+ end
62
+
63
+ it "should supported function 'read coils'" do
64
+ @client.read_coils(0,3).should == @server.coils[0,3]
65
+ end
66
+
67
+ it "should supported function 'read discrete inputs'" do
68
+ @client.read_discrete_inputs(1,3).should == @server.discret_inputs[1,3]
69
+ end
70
+
71
+ it "should supported function 'read holding registers'" do
72
+ @client.read_holding_registers(0,3).should == @server.holding_registers[0,3]
73
+ end
74
+
75
+ it "should supported function 'read input registers'" do
76
+ @client.read_input_registers(2,2).should == @server.input_registers[2,2]
77
+ end
78
+
79
+ it "should supported function 'write single coil'" do
80
+ @server.coils[3] = 0
81
+ @client.write_single_coil(3,1)
82
+ @server.coils[3].should == 1
83
+ end
84
+
85
+ it "should supported function 'write single register'" do
86
+ @server.holding_registers[3] = 25
87
+ @client.write_single_register(3,35)
88
+ @server.holding_registers[3].should == 35
89
+ end
90
+
91
+ it "should supported function 'write multiple coils'" do
92
+ @server.coils = [1,1,1,0, 0,0,0,0, 0,0,0,0, 0,1,1,1]
93
+ @client.write_multiple_coils(3, [1, 0,1,0,1, 0,1,0,1])
94
+ @server.coils.should == [1,1,1,1, 0,1,0,1, 0,1,0,1, 0,1,1,1]
95
+ end
96
+
97
+ it "should supported function 'write multiple registers'" do
98
+ @server.holding_registers = [1,2,3,4,5,6,7,8,9]
99
+ @client.write_multiple_registers(3,[1,2,3,4,5])
100
+ @server.holding_registers.should == [1,2,3,1,2,3,4,5,9]
101
+ end
102
+
103
+ after do
104
+ @client.close if @client
105
+ @server.stop if @server
106
+ end
107
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sanderjd-rmodbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksey Timin
@@ -19,12 +19,28 @@ description: A free Ruby implementation of the ModBus protocol
19
19
  email: sanderjd@gmail.com
20
20
  executables: []
21
21
 
22
- extensions:
23
- - ext/extconf.rb
22
+ extensions: []
23
+
24
24
  extra_rdoc_files: []
25
25
 
26
26
  files:
27
- - ext/extconf.rb
27
+ - VERSION.yml
28
+ - AUTHORS
29
+ - CHANGES
30
+ - LICENSE
31
+ - README
32
+ - Rakefile
33
+ - lib/rmodbus
34
+ - lib/rmodbus/client.rb
35
+ - lib/rmodbus/exceptions.rb
36
+ - lib/rmodbus/tcp_client.rb
37
+ - lib/rmodbus/tcp_server.rb
38
+ - lib/rmodbus.rb
39
+ - spec/client_spec.rb
40
+ - spec/ext_spec.rb
41
+ - spec/rtu_client_spec.rb
42
+ - spec/tcp_client_spec.rb
43
+ - spec/tcp_server_spec.rb
28
44
  has_rdoc: true
29
45
  homepage: http://rubyforge.org/var/svn/rmodbus/trunk
30
46
  post_install_message: