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.
- data/AUTHORS +3 -0
- data/CHANGES +12 -0
- data/LICENSE +675 -0
- data/README +49 -0
- data/Rakefile +49 -0
- data/VERSION.yml +4 -0
- data/lib/rmodbus.rb +13 -0
- data/lib/rmodbus/client.rb +247 -0
- data/lib/rmodbus/exceptions.rb +47 -0
- data/lib/rmodbus/tcp_client.rb +72 -0
- data/lib/rmodbus/tcp_server.rb +160 -0
- data/spec/client_spec.rb +57 -0
- data/spec/ext_spec.rb +27 -0
- data/spec/rtu_client_spec.rb +13 -0
- data/spec/tcp_client_spec.rb +48 -0
- data/spec/tcp_server_spec.rb +107 -0
- metadata +20 -4
- data/ext/extconf.rb +0 -13
@@ -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
|
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,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.
|
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
|
-
|
22
|
+
extensions: []
|
23
|
+
|
24
24
|
extra_rdoc_files: []
|
25
25
|
|
26
26
|
files:
|
27
|
-
-
|
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:
|