rmodbus 1.3.2 → 2.1.2
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.
- 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 -308
- 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
|