rmodbus-ccutrer 2.0.0 → 2.1.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS.md +14 -7
  3. data/README.md +8 -8
  4. data/examples/perfomance_rtu.rb +55 -56
  5. data/examples/perfomance_rtu_via_tcp.rb +54 -55
  6. data/examples/perfomance_tcp.rb +54 -55
  7. data/examples/simple_xpca_gateway.rb +85 -0
  8. data/examples/use_rtu_via_tcp_modbus.rb +14 -11
  9. data/examples/use_tcp_modbus.rb +14 -11
  10. data/lib/rmodbus/client/slave.rb +58 -70
  11. data/lib/rmodbus/client.rb +13 -10
  12. data/lib/rmodbus/debug.rb +10 -6
  13. data/lib/rmodbus/errors.rb +26 -2
  14. data/lib/rmodbus/ext.rb +72 -51
  15. data/lib/rmodbus/options.rb +4 -1
  16. data/lib/rmodbus/proxy.rb +14 -9
  17. data/lib/rmodbus/rtu.rb +38 -32
  18. data/lib/rmodbus/rtu_client.rb +5 -2
  19. data/lib/rmodbus/rtu_server.rb +9 -7
  20. data/lib/rmodbus/rtu_slave.rb +6 -2
  21. data/lib/rmodbus/rtu_via_tcp_server.rb +7 -5
  22. data/lib/rmodbus/server/slave.rb +4 -2
  23. data/lib/rmodbus/server.rb +97 -73
  24. data/lib/rmodbus/sp.rb +10 -12
  25. data/lib/rmodbus/tcp.rb +5 -2
  26. data/lib/rmodbus/tcp_client.rb +3 -0
  27. data/lib/rmodbus/tcp_server.rb +28 -26
  28. data/lib/rmodbus/tcp_slave.rb +17 -16
  29. data/lib/rmodbus/version.rb +3 -1
  30. data/lib/rmodbus.rb +20 -18
  31. metadata +50 -49
  32. data/Rakefile +0 -29
  33. data/examples/simple-xpca-gateway.rb +0 -84
  34. data/spec/client_spec.rb +0 -88
  35. data/spec/exception_spec.rb +0 -120
  36. data/spec/ext_spec.rb +0 -52
  37. data/spec/logging_spec.rb +0 -89
  38. data/spec/proxy_spec.rb +0 -74
  39. data/spec/read_rtu_response_spec.rb +0 -92
  40. data/spec/response_mismach_spec.rb +0 -163
  41. data/spec/rtu_client_spec.rb +0 -86
  42. data/spec/rtu_server_spec.rb +0 -31
  43. data/spec/rtu_via_tcp_client_spec.rb +0 -76
  44. data/spec/rtu_via_tcp_server_spec.rb +0 -89
  45. data/spec/slave_spec.rb +0 -55
  46. data/spec/spec_helper.rb +0 -54
  47. data/spec/tcp_client_spec.rb +0 -88
  48. data/spec/tcp_server_spec.rb +0 -158
@@ -1,4 +1,7 @@
1
- require 'timeout'
1
+ # -*- coding: ascii
2
+ # frozen_string_literal: true
3
+
4
+ require "timeout"
2
5
 
3
6
  module ModBus
4
7
  class Client
@@ -8,15 +11,16 @@ module ModBus
8
11
  include Options
9
12
  # Number of times to retry on read and read timeouts
10
13
  attr_accessor :uid
11
- Exceptions = {
12
- 1 => IllegalFunction.new("The function code received in the query is not an allowable action for the server"),
13
- 2 => IllegalDataAddress.new("The data address received in the query is not an allowable address for the server"),
14
- 3 => IllegalDataValue.new("A value contained in the query data field is not an allowable value for server"),
15
- 4 => SlaveDeviceFailure.new("An unrecoverable error occurred while the server was attempting to perform the requested action"),
16
- 5 => Acknowledge.new("The server has accepted the request and is processing it, but a long duration of time will be required to do so"),
17
- 6 => SlaveDeviceBus.new("The server is engaged in processing a long duration program command"),
18
- 8 => MemoryParityError.new("The extended file area failed to pass a consistency check")
19
- }
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
20
24
  def initialize(uid, io)
21
25
  @uid = uid
22
26
  @io = io
@@ -44,7 +48,7 @@ module ModBus
44
48
  # @param [Integer] ncoils number coils
45
49
  # @return [Array] coils
46
50
  def read_coils(addr, ncoils = 1)
47
- query("\x1" + addr.to_word + ncoils.to_word).unpack_bits[0..ncoils-1]
51
+ query("\x01#{addr.to_word}#{ncoils.to_word}").unpack_bits[0..ncoils - 1]
48
52
  end
49
53
 
50
54
  def read_coil(addr)
@@ -60,10 +64,10 @@ module ModBus
60
64
  # @param [Integer] val value coil (0 or other)
61
65
  # @return self
62
66
  def write_single_coil(addr, val)
63
- if val == 0
64
- query("\x5" + addr.to_word + 0.to_word)
67
+ if [0, false].include?(val)
68
+ query("\x05#{addr.to_word}\x00\x00")
65
69
  else
66
- query("\x5" + addr.to_word + 0xff00.to_word)
70
+ query("\x05#{addr.to_word}\xff\x00")
67
71
  end
68
72
  self
69
73
  end
@@ -77,20 +81,20 @@ module ModBus
77
81
  # @param [Integer] addr address first coil
78
82
  # @param [Array] vals written coils
79
83
  def write_multiple_coils(addr, vals)
80
- nbyte = ((vals.size-1) >> 3) + 1
84
+ nbyte = ((vals.size - 1) >> 3) + 1
81
85
  sum = 0
82
86
  (vals.size - 1).downto(0) do |i|
83
- sum = sum << 1
84
- sum |= 1 if vals[i] > 0
87
+ sum <<= 1
88
+ sum |= 1 if vals[i].positive?
85
89
  end
86
90
 
87
- s_val = ""
91
+ s_val = +""
88
92
  nbyte.times do
89
93
  s_val << (sum & 0xff).chr
90
94
  sum >>= 8
91
95
  end
92
96
 
93
- query("\xf" + addr.to_word + vals.size.to_word + nbyte.chr + s_val)
97
+ query("\x0f#{addr.to_word}#{vals.size.to_word}#{nbyte.chr}#{s_val}")
94
98
  self
95
99
  end
96
100
  alias_method :write_coils, :write_multiple_coils
@@ -115,7 +119,7 @@ module ModBus
115
119
  # @param[Integer] ninputs number inputs
116
120
  # @return [Array] inputs
117
121
  def read_discrete_inputs(addr, ninputs = 1)
118
- query("\x2" + addr.to_word + ninputs.to_word).unpack_bits[0..ninputs-1]
122
+ query("\x02#{addr.to_word}#{ninputs.to_word}").unpack_bits[0..ninputs - 1]
119
123
  end
120
124
 
121
125
  def read_discrete_input(addr)
@@ -142,7 +146,7 @@ module ModBus
142
146
  # @param [Integer] nregs number registers
143
147
  # @return [Array] registers
144
148
  def read_input_registers(addr, nregs = 1)
145
- query("\x4" + addr.to_word + nregs.to_word).unpack('n*')
149
+ query("\x04#{addr.to_word}#{nregs.to_word}").unpack("n*")
146
150
  end
147
151
 
148
152
  def read_input_register(addr)
@@ -171,7 +175,7 @@ module ModBus
171
175
  # @param [Integer] nregs number registers
172
176
  # @return [Array] registers
173
177
  def read_holding_registers(addr, nregs = 1)
174
- query("\x3" + addr.to_word + nregs.to_word).unpack('n*')
178
+ query("\x03#{addr.to_word}#{nregs.to_word}").unpack("n*")
175
179
  end
176
180
 
177
181
  def read_holding_register(addr)
@@ -187,12 +191,11 @@ module ModBus
187
191
  # @param [Integer] val written to register
188
192
  # @return self
189
193
  def write_single_register(addr, val)
190
- query("\x6" + addr.to_word + val.to_word)
194
+ query("\x06#{addr.to_word}#{val.to_word}")
191
195
  self
192
196
  end
193
197
  alias_method :write_holding_register, :write_single_register
194
198
 
195
-
196
199
  # Write multiple holding registers
197
200
  #
198
201
  # @example
@@ -202,12 +205,9 @@ module ModBus
202
205
  # @param [Array] val written registers
203
206
  # @return self
204
207
  def write_multiple_registers(addr, vals)
205
- s_val = ""
206
- vals.each do |reg|
207
- s_val << reg.to_word
208
- end
208
+ s_val = vals.map(&:to_word).join
209
209
 
210
- query("\x10" + addr.to_word + vals.size.to_word + (vals.size * 2).chr + s_val)
210
+ query("\x10#{addr.to_word}#{vals.size.to_word}#{(vals.size * 2).chr}#{s_val}")
211
211
  self
212
212
  end
213
213
  alias_method :write_holding_registers, :write_multiple_registers
@@ -220,7 +220,7 @@ module ModBus
220
220
  # @param [Integer] and_mask mask for AND operation
221
221
  # @param [Integer] or_mask mask for OR operation
222
222
  def mask_write_register(addr, and_mask, or_mask)
223
- query("\x16" + addr.to_word + and_mask.to_word + or_mask.to_word)
223
+ query("\x16#{addr.to_word}#{and_mask.to_word}#{or_mask.to_word}")
224
224
  self
225
225
  end
226
226
 
@@ -235,16 +235,16 @@ module ModBus
235
235
  # @param [Array] vals written registers
236
236
  # @return [Array] registers
237
237
  def read_write_multiple_registers(addr_r, nregs, addr_w, vals)
238
- s_val = ""
239
- vals.each do |reg|
240
- s_val << reg.to_word
241
- end
238
+ s_val = vals.map(&:to_word).join
242
239
 
243
- query("\x17" + addr_r.to_word + nregs.to_word +
244
- addr_w.to_word + vals.size.to_word + (vals.size * 2).chr + s_val).unpack('n*')
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*")
245
243
  end
246
244
  alias_method :read_write_holding_registers, :read_write_multiple_registers
247
245
 
246
+ # rubocop:disable Layout/LineLength
247
+
248
248
  # Request pdu to slave device
249
249
  #
250
250
  # @param [String] pdu request to slave
@@ -266,76 +266,64 @@ module ModBus
266
266
  begin
267
267
  ::Timeout.timeout(@read_retry_timeout, ModBusTimeout) do
268
268
  send_pdu(request)
269
- response = read_pdu unless uid == 0
269
+ response = read_pdu unless uid.zero?
270
270
  end
271
- rescue ModBusTimeout => err
271
+ rescue ModBusTimeout
272
272
  log "Timeout of read operation: (#{@read_retries - tried})"
273
273
  tried += 1
274
274
  retry unless tried >= @read_retries
275
275
  raise ModBusTimeout.new, "Timed out during read attempt"
276
276
  end
277
277
 
278
- return nil if response.size == 0
278
+ return nil if response.empty?
279
279
 
280
280
  read_func = response.getbyte(0)
281
281
  if read_func >= 0x80
282
282
  exc_id = response.getbyte(1)
283
- raise Exceptions[exc_id] unless Exceptions[exc_id].nil?
283
+ raise EXCEPTIONS[exc_id] unless EXCEPTIONS[exc_id].nil?
284
284
 
285
285
  raise ModBusException.new, "Unknown error"
286
286
  end
287
287
 
288
288
  check_response_mismatch(request, response) if raise_exception_on_mismatch
289
- response[2..-1]
289
+ response[2..]
290
290
  end
291
+ # rubocop:enable Layout/LineLength
291
292
 
292
293
  private
294
+
293
295
  def check_response_mismatch(request, response)
294
296
  read_func = response.getbyte(0)
295
- data = response[2..-1]
296
- #Mismatch functional code
297
+ data = response[2..]
298
+ # Mismatch functional code
297
299
  send_func = request.getbyte(0)
298
- if read_func != send_func
299
- msg = "Function code is mismatch (expected #{send_func}, got #{read_func})"
300
- end
300
+ msg = "Function code is mismatch (expected #{send_func}, got #{read_func})" if read_func != send_func
301
301
 
302
302
  case read_func
303
- when 1,2
304
- bc = request.getword(3)/8 + 1
305
- if data.size != bc
306
- msg = "Byte count is mismatch (expected #{bc}, got #{data.size} bytes)"
307
- end
308
- when 3,4
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
309
307
  rc = request.getword(3)
310
- if data.size/2 != rc
311
- msg = "Register count is mismatch (expected #{rc}, got #{data.size/2} regs)"
312
- end
313
- when 5,6
308
+ msg = "Register count is mismatch (expected #{rc}, got #{data.size / 2} regs)" if data.size / 2 != rc
309
+ when 5, 6
314
310
  exp_addr = request.getword(1)
315
311
  got_addr = response.getword(1)
316
- if exp_addr != got_addr
317
- msg = "Address is mismatch (expected #{exp_addr}, got #{got_addr})"
318
- end
312
+ msg = "Address is mismatch (expected #{exp_addr}, got #{got_addr})" if exp_addr != got_addr
319
313
 
320
314
  exp_val = request.getword(3)
321
315
  got_val = response.getword(3)
322
- if exp_val != got_val
323
- msg = "Value is mismatch (expected 0x#{exp_val.to_s(16)}, got 0x#{got_val.to_s(16)})"
324
- end
325
- when 15,16
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
326
318
  exp_addr = request.getword(1)
327
319
  got_addr = response.getword(1)
328
- if exp_addr != got_addr
329
- msg = "Address is mismatch (expected #{exp_addr}, got #{got_addr})"
330
- end
320
+ msg = "Address is mismatch (expected #{exp_addr}, got #{got_addr})" if exp_addr != got_addr
331
321
 
332
322
  exp_quant = request.getword(3)
333
323
  got_quant = response.getword(3)
334
- if exp_quant != got_quant
335
- msg = "Quantity is mismatch (expected #{exp_quant}, got #{got_quant})"
336
- end
324
+ msg = "Quantity is mismatch (expected #{exp_quant}, got #{got_quant})" if exp_quant != got_quant
337
325
  else
338
- warn "Fuiction (#{read_func}) is not supported raising response mismatch"
326
+ warn "Function (#{read_func}) is not supported raising response mismatch"
339
327
  end
340
328
 
341
329
  raise ResponseMismatch.new(msg, request, response) if msg
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModBus
2
4
  # @abstract
3
5
  class Client
4
- autoload :Slave, 'rmodbus/client/slave'
6
+ autoload :Slave, "rmodbus/client/slave"
5
7
 
6
8
  include Errors
7
9
  include Debug
@@ -15,9 +17,9 @@ module ModBus
15
17
  # @param *args depends on implementation
16
18
  # @yield return client object and close it before exit
17
19
  # @return [Client] client object
18
- def initialize(*args, &block)
20
+ def initialize(*args)
19
21
  # Defaults
20
- @debug = false
22
+ @logger = nil
21
23
  @raise_exception_on_mismatch = false
22
24
  @read_retry_timeout = 1
23
25
  @read_retries = 1
@@ -47,9 +49,9 @@ module ModBus
47
49
  #
48
50
  # @param [Integer, #read] uid slave devise
49
51
  # @return [Slave] slave object
50
- def with_slave(uid, &block)
52
+ def with_slave(uid)
51
53
  slave = get_slave(uid, @io)
52
- slave.debug = debug
54
+ slave.logger = logger
53
55
  slave.raise_exception_on_mismatch = raise_exception_on_mismatch
54
56
  slave.read_retries = read_retries
55
57
  slave.read_retry_timeout = read_retry_timeout
@@ -72,22 +74,23 @@ module ModBus
72
74
  end
73
75
 
74
76
  protected
75
- def open_connection(*args)
76
- #Stub conn object
77
+
78
+ def open_connection(*)
79
+ # Stub conn object
77
80
  @io = Object.new
78
81
 
79
- @io.instance_eval """
82
+ @io.instance_eval <<~RUBY, __FILE__, __LINE__ + 1
80
83
  def close
81
84
  end
82
85
 
83
86
  def closed?
84
87
  true
85
88
  end
86
- """
89
+ RUBY
87
90
  @io
88
91
  end
89
92
 
90
- def get_slave(uid,io)
93
+ def get_slave(uid, io)
91
94
  Slave.new(uid, io)
92
95
  end
93
96
  end
data/lib/rmodbus/debug.rb CHANGED
@@ -1,16 +1,20 @@
1
- require 'time'
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
2
4
 
3
5
  module ModBus
4
6
  module Debug
5
- attr_accessor :debug, :raise_exception_on_mismatch,
6
- :read_retries, :read_retry_timeout
7
-
7
+ attr_accessor :raise_exception_on_mismatch,
8
+ :read_retries,
9
+ :read_retry_timeout,
10
+ :logger
8
11
 
9
12
  private
13
+
10
14
  # Put log message on standard output
11
15
  # @param [String] msg message for log
12
16
  def log(msg)
13
- $stdout.puts "#{Time.now.utc.iso8601(2)} #{msg}" if @debug
17
+ logger&.debug(msg)
14
18
  end
15
19
 
16
20
  # Convert string of byte to string for log
@@ -19,7 +23,7 @@ module ModBus
19
23
  # @param [String] msg input string
20
24
  # @return [String] readable string of bytes
21
25
  def logging_bytes(msg)
22
- msg.unpack("H*").first.gsub(/\X{2}/, "[\\0]")
26
+ msg.unpack1("H*").gsub(/\X{2}/, "[\\0]")
23
27
  end
24
28
  end
25
29
  end
@@ -1,30 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModBus
2
4
  module Errors
3
- class ProxyException < StandardError
5
+ class ProxyException < RuntimeError
4
6
  end
5
7
 
6
- class ModBusException < StandardError
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
- class String
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
- array_bit = []
11
- self.unpack('b*')[0].each_char do |c|
12
- array_bit << c.to_i
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
- array_bit
16
+ result
15
17
  end
16
18
 
17
19
  # Get word by index
18
- # @param [Integer] i index first bytes of word
20
+ # @param [Integer] index index first bytes of word
19
21
  # @return unpacked word
20
- def getword(i)
21
- self[i,2].unpack('n')[0]
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('n')
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 16bit Fixnum, we turn it into 32bit Int in big-endian order, halving the size
37
- def to_32f
38
- raise "Array requires an even number of elements to pack to 32bits: was #{self.size}" unless self.size.even?
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 16bit Fixnum, we turn it into 32bit Int in little-endian order, halving the size
43
- def to_32f_le
44
- raise "Array requires an even number of elements to pack to 32bits: was #{self.size}" unless self.size.even?
45
- self.each_slice(2).map { |(lsb, msb)| [lsb, msb].pack('n*').unpack('g')[0] }
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 32bit Floats, we turn it into an array of 16bit Fixnums, doubling the size
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
- self.pack('g*').unpack('n*').each_slice(2).map { |arr| arr.reverse }.flatten
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 16bit Fixnum, we turn it into 32bit Float in big-endian order, halving the size
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
- raise "Array requires an even number of elements to pack to 32bits: was #{self.size}" unless self.size.even?
56
- self.each_slice(2).map { |(lsb, msb)| [msb, lsb].pack('n*').unpack('N')[0] }
69
+ even_elements_check
70
+ pack("n*").unpack("l>*")
57
71
  end
58
72
 
59
- # Given an array of 32bit Fixnum, we turn it into an array of 16bit fixnums, doubling the size
60
- def from_32i
61
- self.pack('N*').unpack('n*').each_slice(2).map { |arr| arr.reverse }.flatten
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
- def pack_to_word
65
- word = 0
66
- s = ""
67
- mask = 0x01
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
- self.each do |bit|
70
- word |= mask if bit > 0
71
- mask <<= 1
72
- if mask == 0x100
73
- mask = 0x01
74
- s << word.chr
75
- word = 0
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
- end
78
- unless mask == 0x01
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
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModBus
2
4
  module Options
3
5
  attr_accessor :raise_exception_on_mismatch,
4
- :read_retries, :read_retry_timeout
6
+ :read_retries,
7
+ :read_retry_timeout
5
8
  end
6
9
  end
data/lib/rmodbus/proxy.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModBus
2
4
  # Given a slave and a type of operation, execute a single or multiple read using hash syntax
3
5
  class ReadOnlyProxy
@@ -10,9 +12,9 @@ module ModBus
10
12
  # Note that in the case of multiples, a pluralized version of the method is sent to the slave
11
13
  def [](key)
12
14
  if key.instance_of?(0.class)
13
- @slave.send("read_#{@type}", key)
15
+ @slave.send(:"read_#{@type}", key)
14
16
  elsif key.instance_of?(Range)
15
- @slave.send("read_#{@type}s", key.first, key.count)
17
+ @slave.send(:"read_#{@type}s", key.first, key.count)
16
18
  else
17
19
  raise ModBus::Errors::ProxyException, "Invalid argument, must be integer or range. Was #{key.class}"
18
20
  end
@@ -20,22 +22,25 @@ module ModBus
20
22
  end
21
23
 
22
24
  class ReadWriteProxy < ReadOnlyProxy
23
- # Write single or multiple values to a modbus slave depending on whether a Fixnum or a Range was given.
24
- # Note that in the case of multiples, a pluralized version of the method is sent to the slave. Also when
25
- # writing multiple values, the number of elements must match the number of registers in the range or an exception is raised
25
+ # Write single or multiple values to a modbus slave depending on whether a
26
+ # Fixnum or a Range was given.
27
+ # Note that in the case of multiples, a pluralized version of the method is
28
+ # sent to the slave. Also when writing multiple values, the number of
29
+ # elements must match the number of registers in the range or an exception
30
+ # is raised
26
31
  def []=(key, val)
27
32
  if key.instance_of?(0.class)
28
- @slave.send("write_#{@type}", key, val)
33
+ @slave.send(:"write_#{@type}", key, val)
29
34
  elsif key.instance_of?(Range)
30
35
  if key.count != val.size
31
- raise ModBus::Errors::ProxyException, "The size of the range must match the size of the values (#{key.count} != #{val.size})"
36
+ raise ModBus::Errors::ProxyException,
37
+ "The size of the range must match the size of the values (#{key.count} != #{val.size})"
32
38
  end
33
39
 
34
- @slave.send("write_#{@type}s", key.first, val)
40
+ @slave.send(:"write_#{@type}s", key.first, val)
35
41
  else
36
42
  raise ModBus::Errors::ProxyException, "Invalid argument, must be integer or range. Was #{key.class}"
37
43
  end
38
44
  end
39
45
  end
40
-
41
46
  end