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.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/NEWS.md +19 -0
  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 +333 -0
  11. data/lib/rmodbus/client.rb +15 -10
  12. data/lib/rmodbus/debug.rb +12 -15
  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 +89 -125
  18. data/lib/rmodbus/rtu_client.rb +22 -2
  19. data/lib/rmodbus/rtu_server.rb +16 -12
  20. data/lib/rmodbus/rtu_slave.rb +26 -3
  21. data/lib/rmodbus/rtu_via_tcp_server.rb +12 -19
  22. data/lib/rmodbus/server/slave.rb +18 -0
  23. data/lib/rmodbus/server.rb +227 -84
  24. data/lib/rmodbus/sp.rb +10 -12
  25. data/lib/rmodbus/tcp.rb +9 -10
  26. data/lib/rmodbus/tcp_client.rb +3 -0
  27. data/lib/rmodbus/tcp_server.rb +41 -35
  28. data/lib/rmodbus/tcp_slave.rb +19 -18
  29. data/lib/rmodbus/version.rb +3 -2
  30. data/lib/rmodbus.rb +20 -21
  31. metadata +63 -50
  32. data/Rakefile +0 -29
  33. data/examples/simple-xpca-gateway.rb +0 -84
  34. data/lib/rmodbus/rtu_via_tcp_client.rb +0 -26
  35. data/lib/rmodbus/rtu_via_tcp_slave.rb +0 -29
  36. data/lib/rmodbus/slave.rb +0 -308
  37. data/spec/client_spec.rb +0 -88
  38. data/spec/exception_spec.rb +0 -119
  39. data/spec/ext_spec.rb +0 -52
  40. data/spec/logging_spec.rb +0 -89
  41. data/spec/proxy_spec.rb +0 -74
  42. data/spec/read_rtu_response_spec.rb +0 -92
  43. data/spec/response_mismach_spec.rb +0 -163
  44. data/spec/rtu_client_spec.rb +0 -86
  45. data/spec/rtu_server_spec.rb +0 -30
  46. data/spec/rtu_via_tcp_client_spec.rb +0 -76
  47. data/spec/rtu_via_tcp_server_spec.rb +0 -16
  48. data/spec/slave_spec.rb +0 -55
  49. data/spec/spec_helper.rb +0 -54
  50. data/spec/tcp_client_spec.rb +0 -88
  51. 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
@@ -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, &block)
20
+ def initialize(*args)
17
21
  # Defaults
18
- @debug = false
22
+ @logger = nil
19
23
  @raise_exception_on_mismatch = false
20
24
  @read_retry_timeout = 1
21
- @read_retries = 10
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, &block)
52
+ def with_slave(uid)
49
53
  slave = get_slave(uid, @io)
50
- slave.debug = debug
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
- def open_connection(*args)
74
- #Stub conn object
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 :debug, :raise_exception_on_mismatch,
4
- :read_retries, :read_retry_timeout
5
-
7
+ attr_accessor :raise_exception_on_mismatch,
8
+ :read_retries,
9
+ :read_retry_timeout,
10
+ :logger
6
11
 
7
12
  private
8
- # Put log message on standart output
13
+
14
+ # Put log message on standard output
9
15
  # @param [String] msg message for log
10
16
  def log(msg)
11
- $stdout.puts msg if @debug
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
- result = ""
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
@@ -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