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.
- 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:
|