rmodbus 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +20 -0
- data/README +8 -10
- data/lib/rmodbus/client.rb +22 -123
- data/lib/rmodbus/ext.rb +61 -0
- data/lib/rmodbus/rtu_client.rb +86 -0
- data/lib/rmodbus/tcp_client.rb +5 -4
- data/lib/rmodbus/tcp_server.rb +19 -32
- data/lib/rmodbus.rb +2 -0
- data/spec/ext_spec.rb +4 -24
- data/spec/rtu_client_spec.rb +40 -0
- data/spec/tcp_client_spec.rb +4 -4
- data/spec/tcp_server_spec.rb +14 -9
- metadata +13 -10
- data/CHANGES +0 -17
data/ChangeLog
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
2009-03-5 Timin Aleksey <atimin@gmail.com>
|
4
|
+
* lib/rmodbus/ext.rb: Added.
|
5
|
+
* lib/rmodbus/*.rb, spec/*_spec.rb: Have done
|
6
|
+
refactoring. Now using pack\unpack methods.
|
7
|
+
* CHANGES: Delete.
|
8
|
+
* ChageLog: Start new log of shanges.
|
9
|
+
|
10
|
+
2009-03-6 Timin Aleksey <atimin@gmail.com>
|
11
|
+
* lib/rmodbus/rtu_client.rb, spec/rtu_client_spec.rb: Have completed to
|
12
|
+
design of ModBus::RTUClient class.
|
13
|
+
|
14
|
+
2009-04-17 James Sanders <sanderjd@gmail.com>
|
15
|
+
* lib/rmodbus/exceptions.rb: Inherit from StandardError instead of
|
16
|
+
Exception
|
17
|
+
2010-01-7 Timin Aleksey <atimin@gmail.com>
|
18
|
+
* lib/rmodbus/tcp_client.rb: Fixed bug of overflow transaction counter (thanks
|
19
|
+
Tallak Tveide)
|
20
|
+
|
data/README
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
|
5
5
|
== Feature
|
6
6
|
|
7
|
-
* Client ModBus-TCP
|
8
|
-
*
|
7
|
+
* Client\Server ModBus-TCP
|
8
|
+
* Client ModBus-RTU (experimental) with serialport gem
|
9
9
|
* Support functions:
|
10
10
|
* 01 (0x01) Read Coils
|
11
11
|
* 02 (0x02) Read Discrete Inputs
|
@@ -15,6 +15,7 @@
|
|
15
15
|
* 06 (0x06) Write Single Register
|
16
16
|
* 15 (0x0F) Write Multiple Coils
|
17
17
|
* 16 (0x10) Write Multiple registers
|
18
|
+
* 22 (0x16) Mask Write register
|
18
19
|
|
19
20
|
== Installation
|
20
21
|
|
@@ -30,21 +31,18 @@ Download and install RModBus with the following
|
|
30
31
|
|
31
32
|
$ gem install --remote rmodbus
|
32
33
|
|
33
|
-
==
|
34
|
+
== GitHub
|
34
35
|
|
35
|
-
You can checkout source code from
|
36
|
+
You can checkout source code from GitHub repositry
|
36
37
|
|
37
|
-
|
38
|
-
$ svn checkout svn://rubyforge.org/var/svn/rmodbu/trunk
|
39
|
-
|
40
|
-
or
|
41
|
-
|
42
|
-
$ svn checkout http://rubyforge.org/var/svn/rmodbus/trunk
|
38
|
+
$ git clone git://github.com/flipback/RModBus.git
|
43
39
|
|
44
40
|
== Reference
|
45
41
|
|
46
42
|
RModBus project: http://rubyforge.org/projects/rmodbus
|
47
43
|
|
44
|
+
RModBud on GitHub: http://github.com/flipback/RModBus
|
45
|
+
|
48
46
|
ModBus community: http://www.modbus-ida.org
|
49
47
|
|
50
48
|
|
data/lib/rmodbus/client.rb
CHANGED
@@ -13,113 +13,8 @@
|
|
13
13
|
# GNU General Public License for more details.
|
14
14
|
|
15
15
|
require 'rmodbus/exceptions'
|
16
|
+
require 'rmodbus/ext'
|
16
17
|
|
17
|
-
class String
|
18
|
-
|
19
|
-
|
20
|
-
if RUBY_VERSION.to_f == 1.9
|
21
|
-
def to_array_int16
|
22
|
-
array_int16 = []
|
23
|
-
i = 0
|
24
|
-
while(i < self.bytesize) do
|
25
|
-
array_int16 << self.getbyte(i) * 256 + self.getbyte(i+1)
|
26
|
-
i += 2
|
27
|
-
end
|
28
|
-
array_int16
|
29
|
-
end
|
30
|
-
|
31
|
-
def to_array_bytes
|
32
|
-
array_bytes = []
|
33
|
-
self.each_byte do |b|
|
34
|
-
array_bytes << b
|
35
|
-
end
|
36
|
-
array_bytes
|
37
|
-
end
|
38
|
-
|
39
|
-
def to_int16
|
40
|
-
self.getbyte(0)*256 + self.getbyte(1)
|
41
|
-
end
|
42
|
-
|
43
|
-
else
|
44
|
-
def to_array_int16
|
45
|
-
array_int16 = []
|
46
|
-
i = 0
|
47
|
-
while(i < self.size) do
|
48
|
-
array_int16 << (self[i].to_i)*256 + self[i+1].to_i
|
49
|
-
i += 2
|
50
|
-
end
|
51
|
-
array_int16
|
52
|
-
end
|
53
|
-
|
54
|
-
def to_array_bytes
|
55
|
-
array_bytes = []
|
56
|
-
i = 0
|
57
|
-
while(i<self.size) do
|
58
|
-
array_bytes << self[i].to_i
|
59
|
-
i += 1
|
60
|
-
end
|
61
|
-
array_bytes
|
62
|
-
end
|
63
|
-
|
64
|
-
def to_int16
|
65
|
-
self[0]*256 + self[1]
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
|
70
|
-
def to_array_bit
|
71
|
-
array_bit = []
|
72
|
-
self.each_byte do |byte|
|
73
|
-
mask = 0x01
|
74
|
-
8.times {
|
75
|
-
unless (mask & byte) == 0
|
76
|
-
array_bit << 1
|
77
|
-
else
|
78
|
-
array_bit << 0
|
79
|
-
end
|
80
|
-
mask = mask << 1
|
81
|
-
}
|
82
|
-
end
|
83
|
-
array_bit
|
84
|
-
end
|
85
|
-
|
86
|
-
end
|
87
|
-
|
88
|
-
class Integer
|
89
|
-
def to_bytes
|
90
|
-
(self >> 8).chr + (self & 0xff).chr
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
class Array
|
95
|
-
|
96
|
-
def to_ints16
|
97
|
-
s = ""
|
98
|
-
self.each do |int16|
|
99
|
-
s << int16.to_bytes
|
100
|
-
end
|
101
|
-
s
|
102
|
-
end
|
103
|
-
|
104
|
-
def bits_to_bytes
|
105
|
-
int16 = 0
|
106
|
-
s = ""
|
107
|
-
mask = 0x01
|
108
|
-
|
109
|
-
self.each do |bit|
|
110
|
-
int16 |= mask if bit > 0
|
111
|
-
mask <<= 1
|
112
|
-
if mask == 0x100
|
113
|
-
mask = 0x01
|
114
|
-
s << int16.chr
|
115
|
-
int16 = 0
|
116
|
-
end
|
117
|
-
end
|
118
|
-
s << int16.chr unless mask == 0x01
|
119
|
-
s
|
120
|
-
end
|
121
|
-
|
122
|
-
end
|
123
18
|
|
124
19
|
module ModBus
|
125
20
|
|
@@ -127,21 +22,23 @@ module ModBus
|
|
127
22
|
|
128
23
|
include Errors
|
129
24
|
# Number of times to retry on connection and read timeouts
|
130
|
-
|
131
|
-
READ_RETRIES = 3
|
25
|
+
attr_accessor :connection_retries, :read_retries
|
132
26
|
|
27
|
+
def initialize
|
28
|
+
@connection_retries = 10
|
29
|
+
@read_retries = 10
|
30
|
+
end
|
133
31
|
# Read value *ncoils* coils starting with *addr*
|
134
|
-
#
|
135
32
|
# Return array of their values
|
136
33
|
def read_coils(addr, ncoils)
|
137
|
-
query("\x1" + addr.
|
34
|
+
query("\x1" + addr.to_word + ncoils.to_word).unpack_bits[0..ncoils-1]
|
138
35
|
end
|
139
36
|
|
140
37
|
# Read value *ncoils* discrete inputs starting with *addr*
|
141
38
|
#
|
142
39
|
# Return array of their values
|
143
40
|
def read_discrete_inputs(addr, ncoils)
|
144
|
-
query("\x2" + addr.
|
41
|
+
query("\x2" + addr.to_word + ncoils.to_word).unpack_bits[0..ncoils-1]
|
145
42
|
end
|
146
43
|
|
147
44
|
# Deprecated version of read_discrete_inputs
|
@@ -154,14 +51,14 @@ module ModBus
|
|
154
51
|
#
|
155
52
|
# Return array of their values
|
156
53
|
def read_holding_registers(addr, nreg)
|
157
|
-
query("\x3" + addr.
|
54
|
+
query("\x3" + addr.to_word + nreg.to_word).unpack('n*')
|
158
55
|
end
|
159
56
|
|
160
57
|
# Read value *nreg* input registers starting with *addr*
|
161
58
|
#
|
162
59
|
# Return array of their values
|
163
60
|
def read_input_registers(addr, nreg)
|
164
|
-
query("\x4" + addr.
|
61
|
+
query("\x4" + addr.to_word + nreg.to_word).unpack('n*')
|
165
62
|
end
|
166
63
|
|
167
64
|
# Write *val* in *addr* coil
|
@@ -171,9 +68,9 @@ module ModBus
|
|
171
68
|
# Return self
|
172
69
|
def write_single_coil(addr, val)
|
173
70
|
if val == 0
|
174
|
-
query("\x5" + addr.
|
71
|
+
query("\x5" + addr.to_word + 0.to_word)
|
175
72
|
else
|
176
|
-
query("\x5" + addr.
|
73
|
+
query("\x5" + addr.to_word + 0xff00.to_word)
|
177
74
|
end
|
178
75
|
self
|
179
76
|
end
|
@@ -182,7 +79,7 @@ module ModBus
|
|
182
79
|
#
|
183
80
|
# Return self
|
184
81
|
def write_single_register(addr, val)
|
185
|
-
query("\x6" + addr.
|
82
|
+
query("\x6" + addr.to_word + val.to_word)
|
186
83
|
self
|
187
84
|
end
|
188
85
|
|
@@ -205,7 +102,7 @@ module ModBus
|
|
205
102
|
sum >>= 8
|
206
103
|
end
|
207
104
|
|
208
|
-
query("\xf" + addr.
|
105
|
+
query("\xf" + addr.to_word + val.size.to_word + nbyte.chr + s_val)
|
209
106
|
self
|
210
107
|
end
|
211
108
|
|
@@ -217,10 +114,10 @@ module ModBus
|
|
217
114
|
def write_multiple_registers(addr, val)
|
218
115
|
s_val = ""
|
219
116
|
val.each do |reg|
|
220
|
-
s_val << reg.
|
117
|
+
s_val << reg.to_word
|
221
118
|
end
|
222
119
|
|
223
|
-
query("\x10" + addr.
|
120
|
+
query("\x10" + addr.to_word + val.size.to_word + (val.size * 2).chr + s_val)
|
224
121
|
self
|
225
122
|
end
|
226
123
|
|
@@ -228,7 +125,7 @@ module ModBus
|
|
228
125
|
#
|
229
126
|
# Return self
|
230
127
|
def mask_write_register(addr, and_mask, or_mask)
|
231
|
-
query("\x16" + addr.
|
128
|
+
query("\x16" + addr.to_word + and_mask.to_word + or_mask.to_word)
|
232
129
|
self
|
233
130
|
end
|
234
131
|
|
@@ -239,11 +136,13 @@ module ModBus
|
|
239
136
|
begin
|
240
137
|
timeout(1, ModBusTimeout) do
|
241
138
|
pdu = read_pdu
|
242
|
-
|
139
|
+
end
|
243
140
|
rescue ModBusTimeout => err
|
244
141
|
tried += 1
|
245
|
-
retry unless tried >=
|
246
|
-
raise ModBusTimeout.new,
|
142
|
+
retry unless tried >= @read_retries
|
143
|
+
raise ModBusTimeout.new, "Timed out during read attempt"
|
144
|
+
rescue
|
145
|
+
raise ModBusException.new, "Server did not respond"
|
247
146
|
end
|
248
147
|
|
249
148
|
if pdu[0].to_i >= 0x80
|
data/lib/rmodbus/ext.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# RModBus - free implementation of ModBus protocol on Ruby.
|
2
|
+
#
|
3
|
+
# Copyright (C) 2009 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
|
+
|
15
|
+
class String
|
16
|
+
|
17
|
+
unless RUBY_VERSION.to_f == 1.9
|
18
|
+
def getbyte(index)
|
19
|
+
self[index].to_i
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def unpack_bits
|
24
|
+
array_bit = []
|
25
|
+
self.unpack('b*')[0].each_char do |b|
|
26
|
+
array_bit << b.to_i
|
27
|
+
end
|
28
|
+
array_bit
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
class Integer
|
34
|
+
|
35
|
+
def to_word
|
36
|
+
(self >> 8).chr + (self & 0xff).chr
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
class Array
|
42
|
+
|
43
|
+
def pack_to_word
|
44
|
+
word = 0
|
45
|
+
s = ""
|
46
|
+
mask = 0x01
|
47
|
+
|
48
|
+
self.each do |bit|
|
49
|
+
word |= mask if bit > 0
|
50
|
+
mask <<= 1
|
51
|
+
if mask == 0x100
|
52
|
+
mask = 0x01
|
53
|
+
s << word.chr
|
54
|
+
word = 0
|
55
|
+
end
|
56
|
+
end
|
57
|
+
s << word.chr unless mask == 0x01
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue
|
4
|
+
end
|
5
|
+
require 'serialport'
|
6
|
+
|
7
|
+
module ModBus
|
8
|
+
|
9
|
+
class RTUClient < Client
|
10
|
+
|
11
|
+
def initialize(port, rate=9600, slaveaddr=1)
|
12
|
+
@port = SerialPort.new(port, rate)
|
13
|
+
@slave = slaveaddr
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
def send_pdu(pdu)
|
18
|
+
msg = @slave.chr + pdu
|
19
|
+
msg << crc16(msg).to_word
|
20
|
+
@port.write msg
|
21
|
+
end
|
22
|
+
|
23
|
+
def read_pdu
|
24
|
+
msg = @port.read
|
25
|
+
if msg.getbyte(0) == @slave
|
26
|
+
return msg[1..-3] if msg[-2,2] == crc16(msg[0..-3]).to_word
|
27
|
+
end
|
28
|
+
loop do
|
29
|
+
#waite timeout
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def crc16(msg)
|
34
|
+
crc_lo = 0xff
|
35
|
+
crc_hi = 0xff
|
36
|
+
|
37
|
+
msg.unpack('c*').each do |byte|
|
38
|
+
i = crc_hi ^ byte
|
39
|
+
crc_hi = crc_lo ^ CrcHiTable[i]
|
40
|
+
crc_lo = CrcLoTable[i]
|
41
|
+
end
|
42
|
+
|
43
|
+
return ((crc_hi << 8) + crc_lo)
|
44
|
+
end
|
45
|
+
|
46
|
+
CrcHiTable = [
|
47
|
+
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
|
48
|
+
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
49
|
+
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
|
50
|
+
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
|
51
|
+
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
|
52
|
+
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
|
53
|
+
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
|
54
|
+
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
|
55
|
+
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
|
56
|
+
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
57
|
+
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
|
58
|
+
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
59
|
+
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
|
60
|
+
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
|
61
|
+
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
|
62
|
+
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
|
63
|
+
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
|
64
|
+
0x40]
|
65
|
+
CrcLoTable = [
|
66
|
+
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
|
67
|
+
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
|
68
|
+
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
|
69
|
+
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
|
70
|
+
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
|
71
|
+
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
|
72
|
+
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
|
73
|
+
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
|
74
|
+
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
|
75
|
+
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
|
76
|
+
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
|
77
|
+
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
|
78
|
+
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
|
79
|
+
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
|
80
|
+
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
|
81
|
+
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
|
82
|
+
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
|
83
|
+
0x40]
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
data/lib/rmodbus/tcp_client.rb
CHANGED
@@ -35,7 +35,7 @@ module ModBus
|
|
35
35
|
end
|
36
36
|
rescue ModBusTimeout => err
|
37
37
|
tried += 1
|
38
|
-
retry unless tried >=
|
38
|
+
retry unless tried >= @connection_retries
|
39
39
|
raise ModBusTimeout.new, 'Timed out attempting to create connection'
|
40
40
|
end
|
41
41
|
@slave = slaveaddr
|
@@ -51,16 +51,17 @@ module ModBus
|
|
51
51
|
|
52
52
|
private
|
53
53
|
def send_pdu(pdu)
|
54
|
+
@@transaction = 0 if @@transaction.next > 65535
|
54
55
|
@@transaction += 1
|
55
|
-
@sock.write @@transaction.
|
56
|
+
@sock.write @@transaction.to_word + "\0\0" + (pdu.size + 1).to_word + @slave.chr + pdu
|
56
57
|
end
|
57
58
|
|
58
59
|
def read_pdu
|
59
60
|
header = @sock.read(7)
|
60
61
|
if header
|
61
|
-
tin = header[0,2].
|
62
|
+
tin = header[0,2].unpack('n')[0]
|
62
63
|
raise Errors::ModBusException.new("Transaction number mismatch") unless tin == @@transaction
|
63
|
-
len = header[4,2].
|
64
|
+
len = header[4,2].unpack('n')[0]
|
64
65
|
@sock.read(len-1)
|
65
66
|
else
|
66
67
|
raise Errors::ModBusException.new("Server did not respond")
|
data/lib/rmodbus/tcp_server.rb
CHANGED
@@ -17,12 +17,10 @@ require 'gserver'
|
|
17
17
|
|
18
18
|
module ModBus
|
19
19
|
|
20
|
-
|
20
|
+
class TCPServer < GServer
|
21
21
|
|
22
22
|
attr_accessor :coils, :discret_inputs, :holding_registers, :input_registers
|
23
23
|
|
24
|
-
|
25
|
-
|
26
24
|
Funcs = [1,2,3,4,5,6,15,16]
|
27
25
|
|
28
26
|
def initialize(port = 502, uid = 1)
|
@@ -37,27 +35,16 @@ module ModBus
|
|
37
35
|
def serve(io)
|
38
36
|
loop do
|
39
37
|
req = io.read(7)
|
40
|
-
if
|
41
|
-
|
42
|
-
|
43
|
-
break
|
44
|
-
end
|
45
|
-
else
|
46
|
-
if req[2,2] != "\x00\x00" or req[6].to_i != @uid
|
47
|
-
io.close
|
48
|
-
break
|
49
|
-
end
|
38
|
+
if req[2,2] != "\x00\x00" or req.getbyte(6) != @uid
|
39
|
+
io.close
|
40
|
+
break
|
50
41
|
end
|
51
42
|
|
52
43
|
tr = req[0,2]
|
53
|
-
len = req[4,2].
|
44
|
+
len = req[4,2].unpack('n')[0]
|
54
45
|
req = io.read(len - 1)
|
55
46
|
|
56
|
-
|
57
|
-
func = req.getbyte(0)
|
58
|
-
else
|
59
|
-
func = req[0].to_i
|
60
|
-
end
|
47
|
+
func = req.getbyte(0)
|
61
48
|
|
62
49
|
unless Funcs.include?(func)
|
63
50
|
param = { :err => 1 }
|
@@ -67,24 +54,24 @@ module ModBus
|
|
67
54
|
when 1
|
68
55
|
param = parse_read_func(req, @coils)
|
69
56
|
if param[:err] == 0
|
70
|
-
val = @coils[param[:addr],param[:quant]].
|
57
|
+
val = @coils[param[:addr],param[:quant]].pack_to_word
|
71
58
|
res = func.chr + val.size.chr + val
|
72
59
|
end
|
73
60
|
when 2
|
74
61
|
param = parse_read_func(req, @discret_inputs)
|
75
62
|
if param[:err] == 0
|
76
|
-
val = @discret_inputs[param[:addr],param[:quant]].
|
63
|
+
val = @discret_inputs[param[:addr],param[:quant]].pack_to_word
|
77
64
|
res = func.chr + val.size.chr + val
|
78
65
|
end
|
79
66
|
when 3
|
80
67
|
param = parse_read_func(req, @holding_registers)
|
81
68
|
if param[:err] == 0
|
82
|
-
res = func.chr + (param[:quant] * 2).chr + @holding_registers[param[:addr],param[:quant]].
|
69
|
+
res = func.chr + (param[:quant] * 2).chr + @holding_registers[param[:addr],param[:quant]].pack('n*')
|
83
70
|
end
|
84
71
|
when 4
|
85
72
|
param = parse_read_func(req, @input_registers)
|
86
73
|
if param[:err] == 0
|
87
|
-
res = func.chr + (param[:quant] * 2).chr + @input_registers[param[:addr],param[:quant]].
|
74
|
+
res = func.chr + (param[:quant] * 2).chr + @input_registers[param[:addr],param[:quant]].pack('n*')
|
88
75
|
end
|
89
76
|
when 5
|
90
77
|
param = parse_write_coil_func(req)
|
@@ -112,7 +99,7 @@ module ModBus
|
|
112
99
|
end
|
113
100
|
end
|
114
101
|
if param[:err] == 0
|
115
|
-
resp = tr + "\0\0" + (res.size + 1).
|
102
|
+
resp = tr + "\0\0" + (res.size + 1).to_word + @uid.chr + res
|
116
103
|
else
|
117
104
|
resp = tr + "\0\0\0\3" + @uid.chr + (func | 0x80).chr + param[:err].chr
|
118
105
|
end
|
@@ -123,21 +110,21 @@ module ModBus
|
|
123
110
|
private
|
124
111
|
|
125
112
|
def parse_read_func(req, field)
|
126
|
-
quant = req[3,2].
|
113
|
+
quant = req[3,2].unpack('n')[0]
|
127
114
|
|
128
115
|
return { :err => 3} unless quant <= 0x7d
|
129
116
|
|
130
|
-
addr = req[1,2].
|
117
|
+
addr = req[1,2].unpack('n')[0]
|
131
118
|
return { :err => 2 } unless addr + quant <= field.size
|
132
119
|
|
133
120
|
return { :err => 0, :quant => quant, :addr => addr }
|
134
121
|
end
|
135
122
|
|
136
123
|
def parse_write_coil_func(req)
|
137
|
-
addr = req[1,2].
|
124
|
+
addr = req[1,2].unpack('n')[0]
|
138
125
|
return { :err => 2 } unless addr <= @coils.size
|
139
126
|
|
140
|
-
val = req[3,2].
|
127
|
+
val = req[3,2].unpack('n')[0]
|
141
128
|
return { :err => 3 } unless val == 0 or val == 0xff00
|
142
129
|
|
143
130
|
val = 1 if val == 0xff00
|
@@ -145,10 +132,10 @@ module ModBus
|
|
145
132
|
end
|
146
133
|
|
147
134
|
def parse_write_register_func(req)
|
148
|
-
addr = req[1,2].
|
135
|
+
addr = req[1,2].unpack('n')[0]
|
149
136
|
return { :err => 2 } unless addr <= @coils.size
|
150
137
|
|
151
|
-
val = req[3,2].
|
138
|
+
val = req[3,2].unpack('n')[0]
|
152
139
|
|
153
140
|
return { :err => 0, :addr => addr, :val => val }
|
154
141
|
end
|
@@ -157,7 +144,7 @@ module ModBus
|
|
157
144
|
param = parse_read_func(req, @coils)
|
158
145
|
|
159
146
|
if param[:err] == 0
|
160
|
-
param = {:err => 0, :addr => param[:addr], :quant => param[:quant], :val => req[6,param[:quant]].
|
147
|
+
param = {:err => 0, :addr => param[:addr], :quant => param[:quant], :val => req[6,param[:quant]].unpack_bits }
|
161
148
|
end
|
162
149
|
param
|
163
150
|
end
|
@@ -166,7 +153,7 @@ module ModBus
|
|
166
153
|
param = parse_read_func(req, @holding_registers)
|
167
154
|
|
168
155
|
if param[:err] == 0
|
169
|
-
param = {:err => 0, :addr => param[:addr], :quant => param[:quant], :val => req[6,param[:quant] * 2].
|
156
|
+
param = {:err => 0, :addr => param[:addr], :quant => param[:quant], :val => req[6,param[:quant] * 2].unpack('n*')}
|
170
157
|
end
|
171
158
|
param
|
172
159
|
end
|
data/lib/rmodbus.rb
CHANGED
data/spec/ext_spec.rb
CHANGED
@@ -2,33 +2,13 @@ require 'rmodbus'
|
|
2
2
|
|
3
3
|
describe Array do
|
4
4
|
|
5
|
-
|
6
|
-
arr = [1,0,1,1, 0,0,1,1, 1,1,0,1, 0,1,1,0, 1,0,1]
|
7
|
-
arr.bits_to_bytes.should == "\xcd\x6b\x5"
|
8
|
-
end
|
9
|
-
|
10
|
-
it "should return string reprisent 16bit fot 8 bits" do
|
11
|
-
arr = [1,0,1,1, 0,0,1,1]
|
12
|
-
arr.bits_to_bytes.should == "\xcd"
|
5
|
+
before do
|
6
|
+
@arr = [1,0,1,1, 0,0,1,1, 1,1,0,1, 0,1,1,0, 1,0,1]
|
13
7
|
end
|
14
8
|
|
15
|
-
it "should return string reprisent
|
16
|
-
arr
|
17
|
-
arr.to_ints16 == "\x0\x1\x0\x2"
|
9
|
+
it "should return string reprisent 16bit" do
|
10
|
+
@arr.pack_to_word == "\xcd\x6b\x5"
|
18
11
|
end
|
19
12
|
|
20
13
|
end
|
21
14
|
|
22
|
-
describe String do
|
23
|
-
|
24
|
-
it "should return array of int16" do
|
25
|
-
str = "\x1\x2\x3\x4\x5\x6"
|
26
|
-
str.to_array_int16.should == [0x102, 0x304, 0x506]
|
27
|
-
end
|
28
|
-
|
29
|
-
it "should have convert 2 chars to word" do
|
30
|
-
str = "\x1\x1"
|
31
|
-
str.to_int16.should == 257
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue
|
4
|
+
end
|
5
|
+
require 'rmodbus'
|
6
|
+
|
7
|
+
include ModBus
|
8
|
+
|
9
|
+
describe RTUClient do
|
10
|
+
|
11
|
+
before do
|
12
|
+
@port = mock('Serial port')
|
13
|
+
SerialPort.should_receive(:new).with("/dev/port1", 9600).and_return(@port)
|
14
|
+
@mb_client = RTUClient.new("/dev/port1", 9600, 1)
|
15
|
+
@mb_client.read_retries = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should ignore frame with other UID" do
|
19
|
+
request = "\x10\x0\x1\x0\x1\x2\xff\xff"
|
20
|
+
@port.should_receive(:write).with("\1#{request}\xA6\x31")
|
21
|
+
@port.should_receive(:read).and_return("\x2\x10\x0\x1\x0\x1\x1C\x08")
|
22
|
+
lambda {@mb_client.query(request)}.should raise_error(ModBus::Errors::ModBusTimeout)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should ignored frame with incorrect CRC" do
|
26
|
+
request = "\x10\x0\x1\x0\x1\x2\xff\xff"
|
27
|
+
@port.should_receive(:write).with("\1#{request}\xA6\x31")
|
28
|
+
@port.should_receive(:read).and_return("\x1\x10\x0\x1\x0\x1\x1C\x08")
|
29
|
+
lambda {@mb_client.query(request)}.should raise_error(ModBus::Errors::ModBusTimeout)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should return value of registers"do
|
33
|
+
request = "\x3\x0\x1\x0\x1"
|
34
|
+
@port.should_receive(:write).with("\1#{request}\xd5\xca")
|
35
|
+
@port.should_receive(:read).and_return("\x1\x3\x2\xff\xff\xb9\xf4")
|
36
|
+
@mb_client.query(request).should == "\xff\xff"
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
data/spec/tcp_client_spec.rb
CHANGED
@@ -6,7 +6,7 @@ describe TCPClient, "method 'query'" do
|
|
6
6
|
|
7
7
|
UID = 1
|
8
8
|
|
9
|
-
before do
|
9
|
+
before(:each) do
|
10
10
|
@sock = mock("Socket")
|
11
11
|
@adu = "\000\001\000\000\000\001\001"
|
12
12
|
|
@@ -17,14 +17,14 @@ describe TCPClient, "method 'query'" do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
it 'should send valid MBAP Header' do
|
20
|
-
@adu[0,2] = TCPClient.transaction.next.
|
20
|
+
@adu[0,2] = TCPClient.transaction.next.to_word
|
21
21
|
@sock.should_receive(:write).with(@adu)
|
22
22
|
@sock.should_receive(:read).with(7).and_return(@adu)
|
23
23
|
@mb_client.query('').should == nil
|
24
24
|
end
|
25
25
|
|
26
26
|
it 'should throw exception if get other transaction' do
|
27
|
-
@adu[0,2] = TCPClient.transaction.next.
|
27
|
+
@adu[0,2] = TCPClient.transaction.next.to_word
|
28
28
|
@sock.should_receive(:write).with(@adu)
|
29
29
|
@sock.should_receive(:read).with(7).and_return("\000\002\000\000\000\001" + UID.chr)
|
30
30
|
begin
|
@@ -37,7 +37,7 @@ describe TCPClient, "method 'query'" do
|
|
37
37
|
it 'should return only data from PDU' do
|
38
38
|
request = "\x3\x0\x6b\x0\x3"
|
39
39
|
response = "\x3\x6\x2\x2b\x0\x0\x0\x64"
|
40
|
-
@adu = TCPClient.transaction.next.
|
40
|
+
@adu = TCPClient.transaction.next.to_word + "\x0\x0\x0\x9" + UID.chr + request
|
41
41
|
@sock.should_receive(:write).with(@adu[0,4] + "\0\6" + UID.chr + request)
|
42
42
|
@sock.should_receive(:read).with(7).and_return(@adu[0,7])
|
43
43
|
@sock.should_receive(:read).with(8).and_return(response)
|
data/spec/tcp_server_spec.rb
CHANGED
@@ -10,6 +10,7 @@ describe TCPServer do
|
|
10
10
|
@server.input_registers = [1,2,3,4]
|
11
11
|
@server.start
|
12
12
|
@client = ModBus::TCPClient.new('127.0.0.1', 8502, 1)
|
13
|
+
@client.read_retries = 0
|
13
14
|
end
|
14
15
|
|
15
16
|
it "should silent if UID has mismatched" do
|
@@ -20,15 +21,18 @@ describe TCPServer do
|
|
20
21
|
rescue ModBus::Errors::ModBusException => ex
|
21
22
|
ex.message.should == "Server did not respond"
|
22
23
|
end
|
24
|
+
client.close
|
23
25
|
end
|
24
26
|
|
25
27
|
it "should silent if protocol identifer has mismatched" do
|
28
|
+
@client.close
|
26
29
|
client = TCPSocket.new('127.0.0.1', 8502)
|
27
30
|
begin
|
28
31
|
client.write "\0\0\1\0\0\6\1"
|
29
32
|
rescue ModBus::Errors::ModBusException => ex
|
30
33
|
ex.message.should == "Server did not respond"
|
31
34
|
end
|
35
|
+
client.close
|
32
36
|
end
|
33
37
|
|
34
38
|
it "should send exception if function not supported" do
|
@@ -55,6 +59,12 @@ describe TCPServer do
|
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
62
|
+
it "should calc a many requests" do
|
63
|
+
@client.read_coils(1,2)
|
64
|
+
@client.write_multiple_registers(0,[9,9,9,])
|
65
|
+
@client.read_holding_registers(0,3).should == [9,9,9]
|
66
|
+
end
|
67
|
+
|
58
68
|
it "should supported function 'read coils'" do
|
59
69
|
@client.read_coils(0,3).should == @server.coils[0,3]
|
60
70
|
end
|
@@ -95,15 +105,10 @@ describe TCPServer do
|
|
95
105
|
@server.holding_registers.should == [1,2,3,1,2,3,4,5,9]
|
96
106
|
end
|
97
107
|
|
98
|
-
it "should valid respond on query reading more than 8 coils at once bug#26387" do
|
99
|
-
@server.coils = [0,0,1,1,0,0,1,1,0,0]
|
100
|
-
@client.read_coils(0,7)
|
101
|
-
@client.read_coils(1,9)
|
102
|
-
@client.read_coils(0,8)
|
103
|
-
end
|
104
|
-
|
105
108
|
after do
|
106
|
-
@client.close
|
107
|
-
@server.stop
|
109
|
+
@client.close
|
110
|
+
@server.stop unless @server.stopped?
|
111
|
+
while GServer.in_service?(8502)
|
112
|
+
end
|
108
113
|
end
|
109
114
|
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: 0.
|
4
|
+
version: 0.3.0
|
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:
|
12
|
+
date: 2010-01-07 00:00:00 +05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -23,23 +23,26 @@ extra_rdoc_files:
|
|
23
23
|
- README
|
24
24
|
- AUTHORS
|
25
25
|
- LICENSE
|
26
|
-
-
|
26
|
+
- ChangeLog
|
27
27
|
files:
|
28
|
-
- lib/rmodbus.rb
|
29
|
-
- lib/rmodbus/exceptions.rb
|
30
28
|
- lib/rmodbus/tcp_client.rb
|
31
|
-
- lib/rmodbus/
|
29
|
+
- lib/rmodbus/rtu_client.rb
|
32
30
|
- lib/rmodbus/tcp_server.rb
|
33
|
-
-
|
31
|
+
- lib/rmodbus/exceptions.rb
|
32
|
+
- lib/rmodbus/client.rb
|
33
|
+
- lib/rmodbus/ext.rb
|
34
|
+
- lib/rmodbus.rb
|
34
35
|
- examples/use_tcp_modbus.rb
|
36
|
+
- examples/add_new_function.rb
|
35
37
|
- spec/ext_spec.rb
|
36
|
-
- spec/client_spec.rb
|
37
38
|
- spec/tcp_client_spec.rb
|
39
|
+
- spec/rtu_client_spec.rb
|
40
|
+
- spec/client_spec.rb
|
38
41
|
- spec/tcp_server_spec.rb
|
39
42
|
- README
|
40
43
|
- AUTHORS
|
41
44
|
- LICENSE
|
42
|
-
-
|
45
|
+
- ChangeLog
|
43
46
|
has_rdoc: true
|
44
47
|
homepage:
|
45
48
|
licenses: []
|
@@ -68,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
71
|
requirements: []
|
69
72
|
|
70
73
|
rubyforge_project:
|
71
|
-
rubygems_version: 1.3.
|
74
|
+
rubygems_version: 1.3.5
|
72
75
|
signing_key:
|
73
76
|
specification_version: 3
|
74
77
|
summary: RModBus - free implementation of protocol ModBus
|
data/CHANGES
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
2008-25-01 Release-0.1.1
|
2
|
-
- fixed bug in function "read discrete inputs (0x2)" of ModBus client
|
3
|
-
|
4
|
-
2009-30-01 Release-0.2
|
5
|
-
- add ModBus-TCP server
|
6
|
-
- add retries for tcp connection and read, default of 10 retries, still 1 second timeout
|
7
|
-
- fix typos
|
8
|
-
|
9
|
-
2009-09-02 Release-0.2.1
|
10
|
-
- add support ruby 1.9.1
|
11
|
-
- fix bug in TCPServer class (it don't close connection after first request now)
|
12
|
-
|
13
|
-
2009-17-04 Release-0.2.2
|
14
|
-
- Make exceptions inherit from StandardError instead of Exception
|
15
|
-
|
16
|
-
2009-22-06 Release-0.2.3
|
17
|
-
- fix bug #26387
|