rmodbus 1.3.3 → 2.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/NEWS.md +19 -0
- data/README.md +8 -8
- data/examples/perfomance_rtu.rb +55 -56
- data/examples/perfomance_rtu_via_tcp.rb +54 -55
- data/examples/perfomance_tcp.rb +54 -55
- data/examples/simple_xpca_gateway.rb +85 -0
- data/examples/use_rtu_via_tcp_modbus.rb +14 -11
- data/examples/use_tcp_modbus.rb +14 -11
- data/lib/rmodbus/client/slave.rb +333 -0
- data/lib/rmodbus/client.rb +15 -10
- data/lib/rmodbus/debug.rb +12 -15
- data/lib/rmodbus/errors.rb +26 -2
- data/lib/rmodbus/ext.rb +72 -51
- data/lib/rmodbus/options.rb +4 -1
- data/lib/rmodbus/proxy.rb +14 -9
- data/lib/rmodbus/rtu.rb +89 -125
- data/lib/rmodbus/rtu_client.rb +22 -2
- data/lib/rmodbus/rtu_server.rb +16 -12
- data/lib/rmodbus/rtu_slave.rb +26 -3
- data/lib/rmodbus/rtu_via_tcp_server.rb +12 -19
- data/lib/rmodbus/server/slave.rb +18 -0
- data/lib/rmodbus/server.rb +227 -84
- data/lib/rmodbus/sp.rb +10 -12
- data/lib/rmodbus/tcp.rb +9 -10
- data/lib/rmodbus/tcp_client.rb +3 -0
- data/lib/rmodbus/tcp_server.rb +41 -35
- data/lib/rmodbus/tcp_slave.rb +19 -18
- data/lib/rmodbus/version.rb +3 -2
- data/lib/rmodbus.rb +20 -21
- metadata +63 -50
- data/Rakefile +0 -29
- data/examples/simple-xpca-gateway.rb +0 -84
- data/lib/rmodbus/rtu_via_tcp_client.rb +0 -26
- data/lib/rmodbus/rtu_via_tcp_slave.rb +0 -29
- data/lib/rmodbus/slave.rb +0 -310
- data/spec/client_spec.rb +0 -88
- data/spec/exception_spec.rb +0 -119
- data/spec/ext_spec.rb +0 -52
- data/spec/logging_spec.rb +0 -89
- data/spec/proxy_spec.rb +0 -74
- data/spec/read_rtu_response_spec.rb +0 -92
- data/spec/response_mismach_spec.rb +0 -163
- data/spec/rtu_client_spec.rb +0 -86
- data/spec/rtu_server_spec.rb +0 -30
- data/spec/rtu_via_tcp_client_spec.rb +0 -76
- data/spec/rtu_via_tcp_server_spec.rb +0 -16
- data/spec/slave_spec.rb +0 -55
- data/spec/spec_helper.rb +0 -54
- data/spec/tcp_client_spec.rb +0 -88
- data/spec/tcp_server_spec.rb +0 -129
@@ -0,0 +1,333 @@
|
|
1
|
+
# -*- coding: ascii
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "timeout"
|
5
|
+
|
6
|
+
module ModBus
|
7
|
+
class Client
|
8
|
+
class Slave
|
9
|
+
include Errors
|
10
|
+
include Debug
|
11
|
+
include Options
|
12
|
+
# Number of times to retry on read and read timeouts
|
13
|
+
attr_accessor :uid
|
14
|
+
|
15
|
+
EXCEPTIONS = {
|
16
|
+
1 => IllegalFunction,
|
17
|
+
2 => IllegalDataAddress,
|
18
|
+
3 => IllegalDataValue,
|
19
|
+
4 => SlaveDeviceFailure,
|
20
|
+
5 => Acknowledge,
|
21
|
+
6 => SlaveDeviceBus,
|
22
|
+
8 => MemoryParityError
|
23
|
+
}.freeze
|
24
|
+
def initialize(uid, io)
|
25
|
+
@uid = uid
|
26
|
+
@io = io
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a ModBus::ReadWriteProxy hash interface for coils
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# coils[addr] => [1]
|
33
|
+
# coils[addr1..addr2] => [1, 0, ..]
|
34
|
+
# coils[addr] = 0 => [0]
|
35
|
+
# coils[addr1..addr2] = [1, 0, ..] => [1, 0, ..]
|
36
|
+
#
|
37
|
+
# @return [ReadWriteProxy] proxy object
|
38
|
+
def coils
|
39
|
+
ModBus::ReadWriteProxy.new(self, :coil)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Read coils
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# read_coils(addr, ncoils) => [1, 0, ..]
|
46
|
+
#
|
47
|
+
# @param [Integer] addr address first coil
|
48
|
+
# @param [Integer] ncoils number coils
|
49
|
+
# @return [Array] coils
|
50
|
+
def read_coils(addr, ncoils = 1)
|
51
|
+
query("\x01#{addr.to_word}#{ncoils.to_word}").unpack_bits[0..ncoils - 1]
|
52
|
+
end
|
53
|
+
|
54
|
+
def read_coil(addr)
|
55
|
+
read_coils(addr, 1).first
|
56
|
+
end
|
57
|
+
|
58
|
+
# Write a single coil
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# write_single_coil(1, 0) => self
|
62
|
+
#
|
63
|
+
# @param [Integer] addr address coil
|
64
|
+
# @param [Integer] val value coil (0 or other)
|
65
|
+
# @return self
|
66
|
+
def write_single_coil(addr, val)
|
67
|
+
if [0, false].include?(val)
|
68
|
+
query("\x05#{addr.to_word}\x00\x00")
|
69
|
+
else
|
70
|
+
query("\x05#{addr.to_word}\xff\x00")
|
71
|
+
end
|
72
|
+
self
|
73
|
+
end
|
74
|
+
alias_method :write_coil, :write_single_coil
|
75
|
+
|
76
|
+
# Write multiple coils
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# write_multiple_coils(1, [0,1,0,1]) => self
|
80
|
+
#
|
81
|
+
# @param [Integer] addr address first coil
|
82
|
+
# @param [Array] vals written coils
|
83
|
+
def write_multiple_coils(addr, vals)
|
84
|
+
nbyte = ((vals.size - 1) >> 3) + 1
|
85
|
+
sum = 0
|
86
|
+
(vals.size - 1).downto(0) do |i|
|
87
|
+
sum <<= 1
|
88
|
+
sum |= 1 if vals[i].positive?
|
89
|
+
end
|
90
|
+
|
91
|
+
s_val = +""
|
92
|
+
nbyte.times do
|
93
|
+
s_val << (sum & 0xff).chr
|
94
|
+
sum >>= 8
|
95
|
+
end
|
96
|
+
|
97
|
+
query("\x0f#{addr.to_word}#{vals.size.to_word}#{nbyte.chr}#{s_val}")
|
98
|
+
self
|
99
|
+
end
|
100
|
+
alias_method :write_coils, :write_multiple_coils
|
101
|
+
|
102
|
+
# Returns a ModBus::ReadOnlyProxy hash interface for discrete inputs
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# discrete_inputs[addr] => [1]
|
106
|
+
# discrete_inputs[addr1..addr2] => [1, 0, ..]
|
107
|
+
#
|
108
|
+
# @return [ReadOnlyProxy] proxy object
|
109
|
+
def discrete_inputs
|
110
|
+
ModBus::ReadOnlyProxy.new(self, :discrete_input)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Read discrete inputs
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# read_discrete_inputs(addr, ninputs) => [1, 0, ..]
|
117
|
+
#
|
118
|
+
# @param [Integer] addr address first input
|
119
|
+
# @param[Integer] ninputs number inputs
|
120
|
+
# @return [Array] inputs
|
121
|
+
def read_discrete_inputs(addr, ninputs = 1)
|
122
|
+
query("\x02#{addr.to_word}#{ninputs.to_word}").unpack_bits[0..ninputs - 1]
|
123
|
+
end
|
124
|
+
|
125
|
+
def read_discrete_input(addr)
|
126
|
+
read_discrete_inputs(addr, 1).first
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns a read/write ModBus::ReadOnlyProxy hash interface for coils
|
130
|
+
#
|
131
|
+
# @example
|
132
|
+
# input_registers[addr] => [1]
|
133
|
+
# input_registers[addr1..addr2] => [1, 0, ..]
|
134
|
+
#
|
135
|
+
# @return [ReadOnlyProxy] proxy object
|
136
|
+
def input_registers
|
137
|
+
ModBus::ReadOnlyProxy.new(self, :input_register)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Read input registers
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# read_input_registers(1, 5) => [1, 0, ..]
|
144
|
+
#
|
145
|
+
# @param [Integer] addr address first registers
|
146
|
+
# @param [Integer] nregs number registers
|
147
|
+
# @return [Array] registers
|
148
|
+
def read_input_registers(addr, nregs = 1)
|
149
|
+
query("\x04#{addr.to_word}#{nregs.to_word}").unpack("n*")
|
150
|
+
end
|
151
|
+
|
152
|
+
def read_input_register(addr)
|
153
|
+
read_input_registers(addr, 1).first
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns a ModBus::ReadWriteProxy hash interface for holding registers
|
157
|
+
#
|
158
|
+
# @example
|
159
|
+
# holding_registers[addr] => [123]
|
160
|
+
# holding_registers[addr1..addr2] => [123, 234, ..]
|
161
|
+
# holding_registers[addr] = 123 => 123
|
162
|
+
# holding_registers[addr1..addr2] = [234, 345, ..] => [234, 345, ..]
|
163
|
+
#
|
164
|
+
# @return [ReadWriteProxy] proxy object
|
165
|
+
def holding_registers
|
166
|
+
ModBus::ReadWriteProxy.new(self, :holding_register)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Read holding registers
|
170
|
+
#
|
171
|
+
# @example
|
172
|
+
# read_holding_registers(1, 5) => [1, 0, ..]
|
173
|
+
#
|
174
|
+
# @param [Integer] addr address first registers
|
175
|
+
# @param [Integer] nregs number registers
|
176
|
+
# @return [Array] registers
|
177
|
+
def read_holding_registers(addr, nregs = 1)
|
178
|
+
query("\x03#{addr.to_word}#{nregs.to_word}").unpack("n*")
|
179
|
+
end
|
180
|
+
|
181
|
+
def read_holding_register(addr)
|
182
|
+
read_holding_registers(addr, 1).first
|
183
|
+
end
|
184
|
+
|
185
|
+
# Write a single holding register
|
186
|
+
#
|
187
|
+
# @example
|
188
|
+
# write_single_register(1, 0xaa) => self
|
189
|
+
#
|
190
|
+
# @param [Integer] addr address registers
|
191
|
+
# @param [Integer] val written to register
|
192
|
+
# @return self
|
193
|
+
def write_single_register(addr, val)
|
194
|
+
query("\x06#{addr.to_word}#{val.to_word}")
|
195
|
+
self
|
196
|
+
end
|
197
|
+
alias_method :write_holding_register, :write_single_register
|
198
|
+
|
199
|
+
# Write multiple holding registers
|
200
|
+
#
|
201
|
+
# @example
|
202
|
+
# write_multiple_registers(1, [0xaa, 0]) => self
|
203
|
+
#
|
204
|
+
# @param [Integer] addr address first registers
|
205
|
+
# @param [Array] val written registers
|
206
|
+
# @return self
|
207
|
+
def write_multiple_registers(addr, vals)
|
208
|
+
s_val = vals.map(&:to_word).join
|
209
|
+
|
210
|
+
query("\x10#{addr.to_word}#{vals.size.to_word}#{(vals.size * 2).chr}#{s_val}")
|
211
|
+
self
|
212
|
+
end
|
213
|
+
alias_method :write_holding_registers, :write_multiple_registers
|
214
|
+
|
215
|
+
# Mask a holding register
|
216
|
+
#
|
217
|
+
# @example
|
218
|
+
# mask_write_register(1, 0xAAAA, 0x00FF) => self
|
219
|
+
# @param [Integer] addr address registers
|
220
|
+
# @param [Integer] and_mask mask for AND operation
|
221
|
+
# @param [Integer] or_mask mask for OR operation
|
222
|
+
def mask_write_register(addr, and_mask, or_mask)
|
223
|
+
query("\x16#{addr.to_word}#{and_mask.to_word}#{or_mask.to_word}")
|
224
|
+
self
|
225
|
+
end
|
226
|
+
|
227
|
+
# Read/write multiple holding registers
|
228
|
+
#
|
229
|
+
# @example
|
230
|
+
# read_write_multiple_registers(1, 5, 1, [0xaa, 0]) => [1, 0, ..]
|
231
|
+
#
|
232
|
+
# @param [Integer] addr_r address first registers to read
|
233
|
+
# @param [Integer] nregs number registers to read
|
234
|
+
# @param [Integer] addr_w address first registers to write
|
235
|
+
# @param [Array] vals written registers
|
236
|
+
# @return [Array] registers
|
237
|
+
def read_write_multiple_registers(addr_r, nregs, addr_w, vals)
|
238
|
+
s_val = vals.map(&:to_word).join
|
239
|
+
|
240
|
+
query("\x17#{addr_r.to_word}#{nregs.to_word}#{addr_w.to_word}" \
|
241
|
+
"#{vals.size.to_word}#{(vals.size * 2).chr}#{s_val}")
|
242
|
+
.unpack("n*")
|
243
|
+
end
|
244
|
+
alias_method :read_write_holding_registers, :read_write_multiple_registers
|
245
|
+
|
246
|
+
# rubocop:disable Layout/LineLength
|
247
|
+
|
248
|
+
# Request pdu to slave device
|
249
|
+
#
|
250
|
+
# @param [String] pdu request to slave
|
251
|
+
# @return [String] received data
|
252
|
+
#
|
253
|
+
# @raise [ResponseMismatch] the received echo response differs from the request
|
254
|
+
# @raise [ModBusTimeout] timed out during read attempt
|
255
|
+
# @raise [ModBusException] unknown error
|
256
|
+
# @raise [IllegalFunction] function code received in the query is not an allowable action for the server
|
257
|
+
# @raise [IllegalDataAddress] data address received in the query is not an allowable address for the server
|
258
|
+
# @raise [IllegalDataValue] value contained in the query data field is not an allowable value for server
|
259
|
+
# @raise [SlaveDeviceFailure] unrecoverable error occurred while the server was attempting to perform the requested action
|
260
|
+
# @raise [Acknowledge] server has accepted the request and is processing it, but a long duration of time will be required to do so
|
261
|
+
# @raise [SlaveDeviceBus] server is engaged in processing a long duration program command
|
262
|
+
# @raise [MemoryParityError] extended file area failed to pass a consistency check
|
263
|
+
def query(request)
|
264
|
+
tried = 0
|
265
|
+
response = ""
|
266
|
+
begin
|
267
|
+
::Timeout.timeout(@read_retry_timeout, ModBusTimeout) do
|
268
|
+
send_pdu(request)
|
269
|
+
response = read_pdu unless uid.zero?
|
270
|
+
end
|
271
|
+
rescue ModBusTimeout
|
272
|
+
log "Timeout of read operation: (#{@read_retries - tried})"
|
273
|
+
tried += 1
|
274
|
+
retry unless tried >= @read_retries
|
275
|
+
raise ModBusTimeout.new, "Timed out during read attempt"
|
276
|
+
end
|
277
|
+
|
278
|
+
return nil if response.empty?
|
279
|
+
|
280
|
+
read_func = response.getbyte(0)
|
281
|
+
if read_func >= 0x80
|
282
|
+
exc_id = response.getbyte(1)
|
283
|
+
raise EXCEPTIONS[exc_id] unless EXCEPTIONS[exc_id].nil?
|
284
|
+
|
285
|
+
raise ModBusException.new, "Unknown error"
|
286
|
+
end
|
287
|
+
|
288
|
+
check_response_mismatch(request, response) if raise_exception_on_mismatch
|
289
|
+
response[2..]
|
290
|
+
end
|
291
|
+
# rubocop:enable Layout/LineLength
|
292
|
+
|
293
|
+
private
|
294
|
+
|
295
|
+
def check_response_mismatch(request, response)
|
296
|
+
read_func = response.getbyte(0)
|
297
|
+
data = response[2..]
|
298
|
+
# Mismatch functional code
|
299
|
+
send_func = request.getbyte(0)
|
300
|
+
msg = "Function code is mismatch (expected #{send_func}, got #{read_func})" if read_func != send_func
|
301
|
+
|
302
|
+
case read_func
|
303
|
+
when 1, 2
|
304
|
+
bc = (request.getword(3) / 8) + 1
|
305
|
+
msg = "Byte count is mismatch (expected #{bc}, got #{data.size} bytes)" if data.size != bc
|
306
|
+
when 3, 4
|
307
|
+
rc = request.getword(3)
|
308
|
+
msg = "Register count is mismatch (expected #{rc}, got #{data.size / 2} regs)" if data.size / 2 != rc
|
309
|
+
when 5, 6
|
310
|
+
exp_addr = request.getword(1)
|
311
|
+
got_addr = response.getword(1)
|
312
|
+
msg = "Address is mismatch (expected #{exp_addr}, got #{got_addr})" if exp_addr != got_addr
|
313
|
+
|
314
|
+
exp_val = request.getword(3)
|
315
|
+
got_val = response.getword(3)
|
316
|
+
msg = "Value is mismatch (expected 0x#{exp_val.to_s(16)}, got 0x#{got_val.to_s(16)})" if exp_val != got_val
|
317
|
+
when 15, 16
|
318
|
+
exp_addr = request.getword(1)
|
319
|
+
got_addr = response.getword(1)
|
320
|
+
msg = "Address is mismatch (expected #{exp_addr}, got #{got_addr})" if exp_addr != got_addr
|
321
|
+
|
322
|
+
exp_quant = request.getword(3)
|
323
|
+
got_quant = response.getword(3)
|
324
|
+
msg = "Quantity is mismatch (expected #{exp_quant}, got #{got_quant})" if exp_quant != got_quant
|
325
|
+
else
|
326
|
+
warn "Function (#{read_func}) is not supported raising response mismatch"
|
327
|
+
end
|
328
|
+
|
329
|
+
raise ResponseMismatch.new(msg, request, response) if msg
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
data/lib/rmodbus/client.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ModBus
|
2
4
|
# @abstract
|
3
5
|
class Client
|
6
|
+
autoload :Slave, "rmodbus/client/slave"
|
7
|
+
|
4
8
|
include Errors
|
5
9
|
include Debug
|
6
10
|
include Options
|
@@ -13,12 +17,12 @@ module ModBus
|
|
13
17
|
# @param *args depends on implementation
|
14
18
|
# @yield return client object and close it before exit
|
15
19
|
# @return [Client] client object
|
16
|
-
def initialize(*args
|
20
|
+
def initialize(*args)
|
17
21
|
# Defaults
|
18
|
-
@
|
22
|
+
@logger = nil
|
19
23
|
@raise_exception_on_mismatch = false
|
20
24
|
@read_retry_timeout = 1
|
21
|
-
@read_retries =
|
25
|
+
@read_retries = 1
|
22
26
|
|
23
27
|
@io = open_connection(*args)
|
24
28
|
if block_given?
|
@@ -45,9 +49,9 @@ module ModBus
|
|
45
49
|
#
|
46
50
|
# @param [Integer, #read] uid slave devise
|
47
51
|
# @return [Slave] slave object
|
48
|
-
def with_slave(uid
|
52
|
+
def with_slave(uid)
|
49
53
|
slave = get_slave(uid, @io)
|
50
|
-
slave.
|
54
|
+
slave.logger = logger
|
51
55
|
slave.raise_exception_on_mismatch = raise_exception_on_mismatch
|
52
56
|
slave.read_retries = read_retries
|
53
57
|
slave.read_retry_timeout = read_retry_timeout
|
@@ -70,22 +74,23 @@ module ModBus
|
|
70
74
|
end
|
71
75
|
|
72
76
|
protected
|
73
|
-
|
74
|
-
|
77
|
+
|
78
|
+
def open_connection(*)
|
79
|
+
# Stub conn object
|
75
80
|
@io = Object.new
|
76
81
|
|
77
|
-
@io.instance_eval
|
82
|
+
@io.instance_eval <<~RUBY, __FILE__, __LINE__ + 1
|
78
83
|
def close
|
79
84
|
end
|
80
85
|
|
81
86
|
def closed?
|
82
87
|
true
|
83
88
|
end
|
84
|
-
|
89
|
+
RUBY
|
85
90
|
@io
|
86
91
|
end
|
87
92
|
|
88
|
-
def get_slave(uid,io)
|
93
|
+
def get_slave(uid, io)
|
89
94
|
Slave.new(uid, io)
|
90
95
|
end
|
91
96
|
end
|
data/lib/rmodbus/debug.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
|
1
5
|
module ModBus
|
2
6
|
module Debug
|
3
|
-
attr_accessor :
|
4
|
-
:read_retries,
|
5
|
-
|
7
|
+
attr_accessor :raise_exception_on_mismatch,
|
8
|
+
:read_retries,
|
9
|
+
:read_retry_timeout,
|
10
|
+
:logger
|
6
11
|
|
7
12
|
private
|
8
|
-
|
13
|
+
|
14
|
+
# Put log message on standard output
|
9
15
|
# @param [String] msg message for log
|
10
16
|
def log(msg)
|
11
|
-
|
17
|
+
logger&.debug(msg)
|
12
18
|
end
|
13
19
|
|
14
20
|
# Convert string of byte to string for log
|
@@ -17,16 +23,7 @@ module ModBus
|
|
17
23
|
# @param [String] msg input string
|
18
24
|
# @return [String] readable string of bytes
|
19
25
|
def logging_bytes(msg)
|
20
|
-
|
21
|
-
msg.each_byte do |c|
|
22
|
-
byte = if c < 16
|
23
|
-
'0' + c.to_s(16)
|
24
|
-
else
|
25
|
-
c.to_s(16)
|
26
|
-
end
|
27
|
-
result << "[#{byte}]"
|
28
|
-
end
|
29
|
-
result
|
26
|
+
msg.unpack1("H*").gsub(/\X{2}/, "[\\0]")
|
30
27
|
end
|
31
28
|
end
|
32
29
|
end
|
data/lib/rmodbus/errors.rb
CHANGED
@@ -1,30 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ModBus
|
2
4
|
module Errors
|
3
|
-
class ProxyException <
|
5
|
+
class ProxyException < RuntimeError
|
4
6
|
end
|
5
7
|
|
6
|
-
class ModBusException <
|
8
|
+
class ModBusException < RuntimeError
|
7
9
|
end
|
8
10
|
|
9
11
|
class IllegalFunction < ModBusException
|
12
|
+
def initialize(msg = nil)
|
13
|
+
super(msg || "The function code received in the query is not an allowable action for the server")
|
14
|
+
end
|
10
15
|
end
|
11
16
|
|
12
17
|
class IllegalDataAddress < ModBusException
|
18
|
+
def initialize
|
19
|
+
super("The data address received in the query is not an allowable address for the server")
|
20
|
+
end
|
13
21
|
end
|
14
22
|
|
15
23
|
class IllegalDataValue < ModBusException
|
24
|
+
def initialize
|
25
|
+
super("A value contained in the query data field is not an allowable value for server")
|
26
|
+
end
|
16
27
|
end
|
17
28
|
|
18
29
|
class SlaveDeviceFailure < ModBusException
|
30
|
+
def initialize
|
31
|
+
super("An unrecoverable error occurred while the server was attempting to perform the requested action")
|
32
|
+
end
|
19
33
|
end
|
20
34
|
|
21
35
|
class Acknowledge < ModBusException
|
36
|
+
def initialize
|
37
|
+
super("The server has accepted the request and is processing it, but a long duration of time will be required to do so") # rubocop:disable Layout/LineLength
|
38
|
+
end
|
22
39
|
end
|
23
40
|
|
24
41
|
class SlaveDeviceBus < ModBusException
|
42
|
+
def initialize
|
43
|
+
super("The server is engaged in processing a long duration program command")
|
44
|
+
end
|
25
45
|
end
|
26
46
|
|
27
47
|
class MemoryParityError < ModBusException
|
48
|
+
def initialize
|
49
|
+
super("The extended file area failed to pass a consistency check")
|
50
|
+
end
|
28
51
|
end
|
29
52
|
|
30
53
|
class ModBusTimeout < ModBusException
|
@@ -32,6 +55,7 @@ module ModBus
|
|
32
55
|
|
33
56
|
class ResponseMismatch < ModBusException
|
34
57
|
attr_reader :request, :response
|
58
|
+
|
35
59
|
def initialize(msg, request, response)
|
36
60
|
super(msg)
|
37
61
|
@request = request
|
data/lib/rmodbus/ext.rb
CHANGED
@@ -1,85 +1,106 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
if RUBY_VERSION < "1.9"
|
4
|
-
def getbyte(index)
|
5
|
-
self[index].to_i
|
6
|
-
end
|
7
|
-
end
|
1
|
+
# frozen_string_literal: true
|
8
2
|
|
3
|
+
class String
|
4
|
+
# unpack a string of bytes into an array of integers (0 or 1)
|
5
|
+
# representing the bits in those bytes, according to how the
|
6
|
+
# ModBus protocol represents coils.
|
9
7
|
def unpack_bits
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
result = []
|
9
|
+
each_byte do |b|
|
10
|
+
8.times do
|
11
|
+
# least significant bits first within each byte
|
12
|
+
result << (b & 0x01)
|
13
|
+
b >>= 1
|
14
|
+
end
|
13
15
|
end
|
14
|
-
|
16
|
+
result
|
15
17
|
end
|
16
18
|
|
17
19
|
# Get word by index
|
18
|
-
# @param [Integer]
|
20
|
+
# @param [Integer] index index first bytes of word
|
19
21
|
# @return unpacked word
|
20
|
-
def getword(
|
21
|
-
self[
|
22
|
+
def getword(index)
|
23
|
+
self[index, 2].unpack1("n")
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
25
27
|
class Integer
|
26
|
-
|
27
|
-
# Shortcut or turning an integer into a word
|
28
|
+
# Shortcut for turning an integer into a word
|
28
29
|
def to_word
|
29
|
-
[self].pack(
|
30
|
+
[self].pack("n")
|
30
31
|
end
|
31
|
-
|
32
32
|
end
|
33
33
|
|
34
34
|
class Array
|
35
|
+
# Swap every pair of elements
|
36
|
+
def byteswap
|
37
|
+
even_elements_check
|
38
|
+
each_slice(2).flat_map(&:reverse)
|
39
|
+
end
|
40
|
+
alias_method :wordswap, :byteswap
|
35
41
|
|
36
|
-
# Given an array of
|
37
|
-
def
|
38
|
-
|
39
|
-
self.each_slice(2).map { |(lsb, msb)| [msb, lsb].pack('n*').unpack('g')[0] }
|
42
|
+
# Given an array of 16-bit unsigned integers, turn it into an array of 16-bit signed integers.
|
43
|
+
def to_16i
|
44
|
+
pack("n*").unpack("s>*")
|
40
45
|
end
|
41
46
|
|
42
|
-
# Given an array of
|
43
|
-
|
44
|
-
|
45
|
-
|
47
|
+
# Given an array of 16-bit unsigned integers, turn it into an array of 32-bit floats, halving the size.
|
48
|
+
# The pairs of 16-bit elements should be in big endian order.
|
49
|
+
def to_32f
|
50
|
+
even_elements_check
|
51
|
+
pack("n*").unpack("g*")
|
46
52
|
end
|
47
53
|
|
48
|
-
# Given an array of
|
54
|
+
# Given an array of 32-bit floats, turn it into an array of 16-bit unsigned integers, doubling the size.
|
49
55
|
def from_32f
|
50
|
-
|
56
|
+
pack("g*").unpack("n*")
|
57
|
+
end
|
58
|
+
|
59
|
+
# Given an array of 16-bit unsigned integers, turn it into 32-bit unsigned integers, halving the size.
|
60
|
+
# The pairs of 16-bit elements should be in big endian order.
|
61
|
+
def to_32u
|
62
|
+
even_elements_check
|
63
|
+
pack("n*").unpack("N*")
|
51
64
|
end
|
52
65
|
|
53
|
-
# Given an array of
|
66
|
+
# Given an array of 16-bit unsigned integers, turn it into 32-bit signed integers, halving the size.
|
67
|
+
# The pairs of 16-bit elements should be in big endian order.
|
54
68
|
def to_32i
|
55
|
-
|
56
|
-
|
69
|
+
even_elements_check
|
70
|
+
pack("n*").unpack("l>*")
|
57
71
|
end
|
58
72
|
|
59
|
-
# Given an array of 32bit
|
60
|
-
def
|
61
|
-
|
73
|
+
# Given an array of 32bit unsigned integers, turn it into an array of 16 bit unsigned integers, doubling the size
|
74
|
+
def from_32u
|
75
|
+
pack("N*").unpack("n*")
|
62
76
|
end
|
63
77
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
78
|
+
# Given an array of 32bit signed integers, turn it into an array of 16 bit unsigned integers, doubling the size
|
79
|
+
def from_32i
|
80
|
+
pack("l>*").unpack("n*")
|
81
|
+
end
|
68
82
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
83
|
+
# pack an array of bits into a string of bytes,
|
84
|
+
# as the ModBus protocol dictates for coils
|
85
|
+
def pack_bits
|
86
|
+
# pack each slice of 8 bits per byte,
|
87
|
+
# forward order (bits 0-7 in byte 0, 8-15 in byte 1, etc.)
|
88
|
+
# non-multiples of 8 are just 0-padded
|
89
|
+
each_slice(8).map do |slice|
|
90
|
+
byte = 0
|
91
|
+
# within each byte, bit 0 is the LSB,
|
92
|
+
# and bit 7 is the MSB
|
93
|
+
slice.reverse_each do |bit|
|
94
|
+
byte <<= 1
|
95
|
+
byte |= 1 if bit.positive?
|
76
96
|
end
|
77
|
-
|
78
|
-
|
79
|
-
s << word.chr
|
80
|
-
else
|
81
|
-
s
|
82
|
-
end
|
97
|
+
byte
|
98
|
+
end.pack("C*")
|
83
99
|
end
|
84
100
|
|
101
|
+
private
|
102
|
+
|
103
|
+
def even_elements_check
|
104
|
+
raise ArgumentError, "Array requires an even number of elements: was #{size}" unless size.even?
|
105
|
+
end
|
85
106
|
end
|