rmodbus 1.3.3 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
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 +32 -61
  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 -310
  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, ..-1]
34
+ # coils[addr] = 0 => [0]
35
+ # coils[addr1..addr2] = [1, 0, ..-1] => [1, 0, ..-1]
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, ..-1]
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, ..-1]
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, ..-1]
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, ..-1]
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, ..-1]
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, ..-1]
161
+ # holding_registers[addr] = 123 => 123
162
+ # holding_registers[addr1..addr2] = [234, 345, ..-1] => [234, 345, ..-1]
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, ..-1]
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, ..-1]
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..-1]
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..-1]
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