rmodbus 0.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGES +5 -0
- data/README +1 -0
- data/examples/add_new_function.rb +19 -0
- data/examples/use_tcp_modbus.rb +17 -0
- data/lib/rmodbus/client.rb +283 -246
- data/lib/rmodbus/tcp_client.rb +3 -4
- data/lib/rmodbus/tcp_server.rb +83 -66
- data/spec/client_spec.rb +57 -0
- data/spec/ext_spec.rb +27 -0
- data/spec/tcp_client_spec.rb +48 -0
- data/spec/tcp_server_spec.rb +106 -0
- metadata +8 -5
- data/ext/extconf.rb +0 -13
- data/ext/serialport.c +0 -1312
- data/lib/rmodbus/rtu_client.rb +0 -6
data/CHANGES
CHANGED
data/README
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue
|
4
|
+
end
|
5
|
+
require 'rmodbus'
|
6
|
+
|
7
|
+
include ModBus
|
8
|
+
|
9
|
+
class MyTCPClient < TCPClient
|
10
|
+
|
11
|
+
def user_define_function(arg1, arg2)
|
12
|
+
query("\x65" + arg1.to_bytes + arg2.to_bytes)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
@my_mb = MyTCPClient.new('127.0.0.1', 502, 1)
|
18
|
+
@my_mb.user_define_function(20,13)
|
19
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib/')
|
2
|
+
require 'rmodbus'
|
3
|
+
|
4
|
+
srv = ModBus::TCPServer.new(8502,1)
|
5
|
+
srv.coils = [1,0,1,1]
|
6
|
+
srv.discret_inputs = [1,1,0,0]
|
7
|
+
srv.holding_registers = [1,2,3,4]
|
8
|
+
srv.input_registers = [1,2,3,4]
|
9
|
+
srv.debug = true
|
10
|
+
srv.audit = true
|
11
|
+
srv.start
|
12
|
+
|
13
|
+
cl = ModBus::TCPClient.new('127.0.0.1', 8502, 1)
|
14
|
+
puts cl.read_holding_registers(0,4)
|
15
|
+
cl.write_multiple_registers(0, [4,4,4])
|
16
|
+
puts cl.read_holding_registers(0,4)
|
17
|
+
srv.stop
|
data/lib/rmodbus/client.rb
CHANGED
@@ -1,246 +1,283 @@
|
|
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
|
-
|
15
|
-
require 'rmodbus/exceptions'
|
16
|
-
|
17
|
-
class String
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
self.
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
#
|
133
|
-
#
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
#
|
153
|
-
#
|
154
|
-
#
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
#
|
190
|
-
#
|
191
|
-
#
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
end
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
end
|
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
|
+
|
15
|
+
require 'rmodbus/exceptions'
|
16
|
+
|
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
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
module ModBus
|
124
|
+
|
125
|
+
class Client
|
126
|
+
|
127
|
+
include Errors
|
128
|
+
# Number of times to retry on connection and read timeouts
|
129
|
+
CONNECTION_RETRIES = 3
|
130
|
+
READ_RETRIES = 3
|
131
|
+
|
132
|
+
# Read value *ncoils* coils starting with *addr*
|
133
|
+
#
|
134
|
+
# Return array of their values
|
135
|
+
def read_coils(addr, ncoils)
|
136
|
+
query("\x1" + addr.to_bytes + ncoils.to_bytes).to_array_bit[0..ncoils-1]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Read value *ncoils* discrete inputs starting with *addr*
|
140
|
+
#
|
141
|
+
# Return array of their values
|
142
|
+
def read_discrete_inputs(addr, ncoils)
|
143
|
+
query("\x2" + addr.to_bytes + ncoils.to_bytes).to_array_bit[0..ncoils-1]
|
144
|
+
end
|
145
|
+
|
146
|
+
# Deprecated version of read_discrete_inputs
|
147
|
+
def read_discret_inputs(addr, ncoils)
|
148
|
+
warn "[DEPRECATION] `read_discret_inputs` is deprecated. Please use `read_discrete_inputs` instead."
|
149
|
+
read_discrete_inputs(addr, ncoils)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Read value *nreg* holding registers starting with *addr*
|
153
|
+
#
|
154
|
+
# Return array of their values
|
155
|
+
def read_holding_registers(addr, nreg)
|
156
|
+
query("\x3" + addr.to_bytes + nreg.to_bytes).to_array_int16
|
157
|
+
end
|
158
|
+
|
159
|
+
# Read value *nreg* input registers starting with *addr*
|
160
|
+
#
|
161
|
+
# Return array of their values
|
162
|
+
def read_input_registers(addr, nreg)
|
163
|
+
query("\x4" + addr.to_bytes + nreg.to_bytes).to_array_int16
|
164
|
+
end
|
165
|
+
|
166
|
+
# Write *val* in *addr* coil
|
167
|
+
#
|
168
|
+
# if *val* lager 0 write 1
|
169
|
+
#
|
170
|
+
# Return self
|
171
|
+
def write_single_coil(addr, val)
|
172
|
+
if val == 0
|
173
|
+
query("\x5" + addr.to_bytes + 0.to_bytes)
|
174
|
+
else
|
175
|
+
query("\x5" + addr.to_bytes + 0xff00.to_bytes)
|
176
|
+
end
|
177
|
+
self
|
178
|
+
end
|
179
|
+
|
180
|
+
# Write *val* in *addr* register
|
181
|
+
#
|
182
|
+
# Return self
|
183
|
+
def write_single_register(addr, val)
|
184
|
+
query("\x6" + addr.to_bytes + val.to_bytes)
|
185
|
+
self
|
186
|
+
end
|
187
|
+
|
188
|
+
# Write *val* in coils starting with *addr*
|
189
|
+
#
|
190
|
+
# *val* it is array of bits
|
191
|
+
#
|
192
|
+
# Return self
|
193
|
+
def write_multiple_coils(addr, val)
|
194
|
+
nbyte = ((val.size-1) >> 3) + 1
|
195
|
+
sum = 0
|
196
|
+
(val.size - 1).downto(0) do |i|
|
197
|
+
sum = sum << 1
|
198
|
+
sum |= 1 if val[i] > 0
|
199
|
+
end
|
200
|
+
|
201
|
+
s_val = ""
|
202
|
+
nbyte.times do
|
203
|
+
s_val << (sum & 0xff).chr
|
204
|
+
sum >>= 8
|
205
|
+
end
|
206
|
+
|
207
|
+
query("\xf" + addr.to_bytes + val.size.to_bytes + nbyte.chr + s_val)
|
208
|
+
self
|
209
|
+
end
|
210
|
+
|
211
|
+
# Write *val* in registers starting with *addr*
|
212
|
+
#
|
213
|
+
# *val* it is array of integer
|
214
|
+
#
|
215
|
+
# Return self
|
216
|
+
def write_multiple_registers(addr, val)
|
217
|
+
s_val = ""
|
218
|
+
val.each do |reg|
|
219
|
+
s_val << reg.to_bytes
|
220
|
+
end
|
221
|
+
|
222
|
+
query("\x10" + addr.to_bytes + val.size.to_bytes + (val.size * 2).chr + s_val)
|
223
|
+
self
|
224
|
+
end
|
225
|
+
|
226
|
+
# Write *current value & and_mask | or mask in *addr* register
|
227
|
+
#
|
228
|
+
# Return self
|
229
|
+
def mask_write_register(addr, and_mask, or_mask)
|
230
|
+
query("\x16" + addr.to_bytes + and_mask.to_bytes + or_mask.to_bytes)
|
231
|
+
self
|
232
|
+
end
|
233
|
+
|
234
|
+
def query(pdu)
|
235
|
+
send_pdu(pdu)
|
236
|
+
|
237
|
+
tried = 0
|
238
|
+
begin
|
239
|
+
timeout(1, ModBusTimeout) do
|
240
|
+
pdu = read_pdu
|
241
|
+
end
|
242
|
+
rescue ModBusTimeout => err
|
243
|
+
tried += 1
|
244
|
+
retry unless tried >= READ_RETRIES
|
245
|
+
raise ModBusTimeout.new, 'Timed out during read attempt'
|
246
|
+
end
|
247
|
+
|
248
|
+
if pdu[0].to_i >= 0x80
|
249
|
+
case pdu[1].to_i
|
250
|
+
when 1
|
251
|
+
raise IllegalFunction.new, "The function code received in the query is not an allowable action for the server"
|
252
|
+
when 2
|
253
|
+
raise IllegalDataAddress.new, "The data address received in the query is not an allowable address for the server"
|
254
|
+
when 3
|
255
|
+
raise IllegalDataValue.new, "A value contained in the query data field is not an allowable value for server"
|
256
|
+
when 4
|
257
|
+
raise SlaveDeviceFailure.new, "An unrecoverable error occurred while the server was attempting to perform the requested action"
|
258
|
+
when 5
|
259
|
+
raise Acknowledge.new, "The server has accepted the request and is processing it, but a long duration of time will be required to do so"
|
260
|
+
when 6
|
261
|
+
raise SlaveDeviceBus.new, "The server is engaged in processing a long duration program command"
|
262
|
+
when 8
|
263
|
+
raise MemoryParityError.new, "The extended file area failed to pass a consistency check"
|
264
|
+
else
|
265
|
+
raise ModBusException.new, "Unknown error"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
pdu[2..-1]
|
269
|
+
end
|
270
|
+
|
271
|
+
protected
|
272
|
+
def send_pdu(pdu)
|
273
|
+
end
|
274
|
+
|
275
|
+
def read_pdu
|
276
|
+
end
|
277
|
+
|
278
|
+
def close
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|