rmodbus 0.2 → 0.2.1
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.
- 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
|