rmodbus 0.2.3 → 0.3.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/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
|