sanderjd-rmodbus 0.1.3 → 0.2.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.
@@ -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: