rmodbus-ccutrer 2.0.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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