rmodbus 0.2 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +5 -0
- data/README +1 -0
- data/examples/add_new_function.rb +19 -0
- data/examples/use_tcp_modbus.rb +17 -0
- data/lib/rmodbus/client.rb +283 -246
- data/lib/rmodbus/tcp_client.rb +3 -4
- data/lib/rmodbus/tcp_server.rb +83 -66
- data/spec/client_spec.rb +57 -0
- data/spec/ext_spec.rb +27 -0
- data/spec/tcp_client_spec.rb +48 -0
- data/spec/tcp_server_spec.rb +106 -0
- metadata +8 -5
- data/ext/extconf.rb +0 -13
- data/ext/serialport.c +0 -1312
- data/lib/rmodbus/rtu_client.rb +0 -6
data/lib/rmodbus/tcp_client.rb
CHANGED
@@ -27,11 +27,11 @@ module ModBus
|
|
27
27
|
|
28
28
|
# Connect with a ModBus server
|
29
29
|
def initialize(ipaddr, port = 502, slaveaddr = 1)
|
30
|
-
|
30
|
+
@ipaddr, @port = ipaddr, port
|
31
31
|
tried = 0
|
32
32
|
begin
|
33
33
|
timeout(1, ModBusTimeout) do
|
34
|
-
@sock = TCPSocket.new(ipaddr, port)
|
34
|
+
@sock = TCPSocket.new(@ipaddr, @port)
|
35
35
|
end
|
36
36
|
rescue ModBusTimeout => err
|
37
37
|
tried += 1
|
@@ -44,7 +44,6 @@ module ModBus
|
|
44
44
|
def close
|
45
45
|
@sock.close unless @sock.closed?
|
46
46
|
end
|
47
|
-
|
48
47
|
|
49
48
|
def self.transaction
|
50
49
|
@@transaction
|
@@ -56,7 +55,7 @@ module ModBus
|
|
56
55
|
@sock.write @@transaction.to_bytes + "\0\0" + (pdu.size + 1).to_bytes + @slave.chr + pdu
|
57
56
|
end
|
58
57
|
|
59
|
-
def read_pdu
|
58
|
+
def read_pdu
|
60
59
|
header = @sock.read(7)
|
61
60
|
if header
|
62
61
|
tin = header[0,2].to_int16
|
data/lib/rmodbus/tcp_server.rb
CHANGED
@@ -21,7 +21,9 @@ module ModBus
|
|
21
21
|
|
22
22
|
attr_accessor :coils, :discret_inputs, :holding_registers, :input_registers
|
23
23
|
|
24
|
-
|
24
|
+
|
25
|
+
|
26
|
+
Funcs = [1,2,3,4,5,6,15,16]
|
25
27
|
|
26
28
|
def initialize(port = 502, uid = 1)
|
27
29
|
@coils = []
|
@@ -33,74 +35,89 @@ module ModBus
|
|
33
35
|
end
|
34
36
|
|
35
37
|
def serve(io)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
38
|
+
loop do
|
39
|
+
req = io.read(7)
|
40
|
+
if RUBY_VERSION.to_f == 1.9
|
41
|
+
if req[2,2] != "\x00\x00" or req.getbyte(6) != @uid
|
42
|
+
io.close
|
43
|
+
break
|
57
44
|
end
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
45
|
+
else
|
46
|
+
if req[2,2] != "\x00\x00" or req[6].to_i != @uid
|
47
|
+
io.close
|
48
|
+
break
|
97
49
|
end
|
50
|
+
end
|
51
|
+
|
52
|
+
tr = req[0,2]
|
53
|
+
len = req[4,2].to_int16
|
54
|
+
req = io.read(len - 1)
|
55
|
+
|
56
|
+
if RUBY_VERSION.to_f == 1.9
|
57
|
+
func = req.getbyte(0)
|
58
|
+
else
|
59
|
+
func = req[0].to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
unless Funcs.include?(func)
|
63
|
+
param = { :err => 1 }
|
64
|
+
end
|
65
|
+
|
66
|
+
case func
|
67
|
+
when 1
|
68
|
+
param = parse_read_func(req, @coils)
|
69
|
+
if param[:err] == 0
|
70
|
+
val = @coils[param[:addr],param[:quant]].bits_to_bytes
|
71
|
+
res = func.chr + val.size.chr + val
|
72
|
+
end
|
73
|
+
when 2
|
74
|
+
param = parse_read_func(req, @discret_inputs)
|
75
|
+
if param[:err] == 0
|
76
|
+
val = @discret_inputs[param[:addr],param[:quant]].bits_to_bytes
|
77
|
+
res = func.chr + val.size.chr + val
|
78
|
+
end
|
79
|
+
when 3
|
80
|
+
param = parse_read_func(req, @holding_registers)
|
81
|
+
if param[:err] == 0
|
82
|
+
res = func.chr + (param[:quant] * 2).chr + @holding_registers[param[:addr],param[:quant]].to_ints16
|
83
|
+
end
|
84
|
+
when 4
|
85
|
+
param = parse_read_func(req, @input_registers)
|
86
|
+
if param[:err] == 0
|
87
|
+
res = func.chr + (param[:quant] * 2).chr + @input_registers[param[:addr],param[:quant]].to_ints16
|
88
|
+
end
|
89
|
+
when 5
|
90
|
+
param = parse_write_coil_func(req)
|
91
|
+
if param[:err] == 0
|
92
|
+
@coils[param[:addr]] = param[:val]
|
93
|
+
res = func.chr + req
|
94
|
+
end
|
95
|
+
when 6
|
96
|
+
param = parse_write_register_func(req)
|
97
|
+
if param[:err] == 0
|
98
|
+
@holding_registers[param[:addr]] = param[:val]
|
99
|
+
res = func.chr + req
|
100
|
+
end
|
101
|
+
when 15
|
102
|
+
param = parse_write_multiple_coils_func(req)
|
103
|
+
if param[:err] == 0
|
104
|
+
@coils[param[:addr],param[:quant]] = param[:val][0,param[:quant]]
|
105
|
+
res = func.chr + req
|
106
|
+
end
|
107
|
+
when 16
|
108
|
+
param = parse_write_multiple_registers_func(req)
|
109
|
+
if param[:err] == 0
|
110
|
+
@holding_registers[param[:addr],param[:quant]] = param[:val][0,param[:quant]]
|
111
|
+
res = func.chr + req
|
112
|
+
end
|
113
|
+
end
|
114
|
+
if param[:err] == 0
|
115
|
+
resp = tr + "\0\0" + (res.size + 1).to_bytes + @uid.chr + res
|
116
|
+
else
|
117
|
+
resp = tr + "\0\0\0\3" + @uid.chr + (func | 0x80).chr + param[:err].chr
|
118
|
+
end
|
119
|
+
io.write resp
|
98
120
|
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
121
|
end
|
105
122
|
|
106
123
|
private
|
data/spec/client_spec.rb
ADDED
@@ -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
|
data/spec/ext_spec.rb
ADDED
@@ -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,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,106 @@
|
|
1
|
+
require 'lib/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
|
+
client = TCPSocket.new('127.0.0.1', 8502)
|
27
|
+
begin
|
28
|
+
client.write "\0\0\1\0\0\6\1"
|
29
|
+
rescue ModBus::Errors::ModBusException => ex
|
30
|
+
ex.message.should == "Server did not respond"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should send exception if function not supported" do
|
35
|
+
begin
|
36
|
+
@client.query('0x43')
|
37
|
+
rescue ModBus::Errors::IllegalFunction => ex
|
38
|
+
ex.message.should == "The function code received in the query is not an allowable action for the server"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should send exception if quanity of out more 0x7d" do
|
43
|
+
begin
|
44
|
+
@client.read_coils(0, 0x7e)
|
45
|
+
rescue ModBus::Errors::IllegalDataValue => ex
|
46
|
+
ex.message.should == "A value contained in the query data field is not an allowable value for server"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should send exception if addr not valid" do
|
51
|
+
begin
|
52
|
+
@client.read_coils(2, 8)
|
53
|
+
rescue ModBus::Errors::IllegalDataAddress => ex
|
54
|
+
ex.message.should == "The data address received in the query is not an allowable address for the server"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should supported function 'read coils'" do
|
59
|
+
@client.read_coils(0,3).should == @server.coils[0,3]
|
60
|
+
end
|
61
|
+
|
62
|
+
after do
|
63
|
+
@server.stop
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should supported function 'read discrete inputs'" do
|
67
|
+
@client.read_discrete_inputs(1,3).should == @server.discret_inputs[1,3]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should supported function 'read holding registers'" do
|
71
|
+
@client.read_holding_registers(0,3).should == @server.holding_registers[0,3]
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should supported function 'read input registers'" do
|
75
|
+
@client.read_input_registers(2,2).should == @server.input_registers[2,2]
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should supported function 'write single coil'" do
|
79
|
+
@server.coils[3] = 0
|
80
|
+
@client.write_single_coil(3,1)
|
81
|
+
@server.coils[3].should == 1
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should supported function 'write single register'" do
|
85
|
+
@server.holding_registers[3] = 25
|
86
|
+
@client.write_single_register(3,35)
|
87
|
+
@server.holding_registers[3].should == 35
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should supported function 'write multiple coils'" do
|
91
|
+
@server.coils = [1,1,1,0, 0,0,0,0, 0,0,0,0, 0,1,1,1]
|
92
|
+
@client.write_multiple_coils(3, [1, 0,1,0,1, 0,1,0,1])
|
93
|
+
@server.coils.should == [1,1,1,1, 0,1,0,1, 0,1,0,1, 0,1,1,1]
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should supported function 'write multiple registers'" do
|
97
|
+
@server.holding_registers = [1,2,3,4,5,6,7,8,9]
|
98
|
+
@client.write_multiple_registers(3,[1,2,3,4,5])
|
99
|
+
@server.holding_registers.should == [1,2,3,1,2,3,4,5,9]
|
100
|
+
end
|
101
|
+
|
102
|
+
after do
|
103
|
+
@client.close if @client
|
104
|
+
@server.stop if @server
|
105
|
+
end
|
106
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rmodbus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- A.Timin, J. Sanders
|
@@ -9,7 +9,7 @@ autorequire: rmodbus
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-02-09 00:00:00 +05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -25,14 +25,17 @@ extra_rdoc_files:
|
|
25
25
|
- LICENSE
|
26
26
|
- CHANGES
|
27
27
|
files:
|
28
|
-
- lib/rmodbus/rtu_client.rb
|
29
28
|
- lib/rmodbus/tcp_server.rb
|
30
29
|
- lib/rmodbus/client.rb
|
31
30
|
- lib/rmodbus/exceptions.rb
|
32
31
|
- lib/rmodbus/tcp_client.rb
|
33
32
|
- lib/rmodbus.rb
|
34
|
-
-
|
35
|
-
-
|
33
|
+
- examples/add_new_function.rb
|
34
|
+
- examples/use_tcp_modbus.rb
|
35
|
+
- spec/client_spec.rb
|
36
|
+
- spec/tcp_client_spec.rb
|
37
|
+
- spec/tcp_server_spec.rb
|
38
|
+
- spec/ext_spec.rb
|
36
39
|
- README
|
37
40
|
- AUTHORS
|
38
41
|
- LICENSE
|