rmodbus-ccutrer 2.0.0

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/NEWS.md +180 -0
  3. data/README.md +115 -0
  4. data/Rakefile +29 -0
  5. data/examples/perfomance_rtu.rb +56 -0
  6. data/examples/perfomance_rtu_via_tcp.rb +55 -0
  7. data/examples/perfomance_tcp.rb +55 -0
  8. data/examples/simple-xpca-gateway.rb +84 -0
  9. data/examples/use_rtu_via_tcp_modbus.rb +22 -0
  10. data/examples/use_tcp_modbus.rb +23 -0
  11. data/lib/rmodbus.rb +21 -0
  12. data/lib/rmodbus/client.rb +94 -0
  13. data/lib/rmodbus/client/slave.rb +345 -0
  14. data/lib/rmodbus/debug.rb +25 -0
  15. data/lib/rmodbus/errors.rb +42 -0
  16. data/lib/rmodbus/ext.rb +85 -0
  17. data/lib/rmodbus/options.rb +6 -0
  18. data/lib/rmodbus/proxy.rb +41 -0
  19. data/lib/rmodbus/rtu.rb +122 -0
  20. data/lib/rmodbus/rtu_client.rb +43 -0
  21. data/lib/rmodbus/rtu_server.rb +48 -0
  22. data/lib/rmodbus/rtu_slave.rb +48 -0
  23. data/lib/rmodbus/rtu_via_tcp_server.rb +35 -0
  24. data/lib/rmodbus/server.rb +246 -0
  25. data/lib/rmodbus/server/slave.rb +16 -0
  26. data/lib/rmodbus/sp.rb +36 -0
  27. data/lib/rmodbus/tcp.rb +31 -0
  28. data/lib/rmodbus/tcp_client.rb +25 -0
  29. data/lib/rmodbus/tcp_server.rb +67 -0
  30. data/lib/rmodbus/tcp_slave.rb +55 -0
  31. data/lib/rmodbus/version.rb +3 -0
  32. data/spec/client_spec.rb +88 -0
  33. data/spec/exception_spec.rb +120 -0
  34. data/spec/ext_spec.rb +52 -0
  35. data/spec/logging_spec.rb +89 -0
  36. data/spec/proxy_spec.rb +74 -0
  37. data/spec/read_rtu_response_spec.rb +92 -0
  38. data/spec/response_mismach_spec.rb +163 -0
  39. data/spec/rtu_client_spec.rb +86 -0
  40. data/spec/rtu_server_spec.rb +31 -0
  41. data/spec/rtu_via_tcp_client_spec.rb +76 -0
  42. data/spec/rtu_via_tcp_server_spec.rb +89 -0
  43. data/spec/slave_spec.rb +55 -0
  44. data/spec/spec_helper.rb +54 -0
  45. data/spec/tcp_client_spec.rb +88 -0
  46. data/spec/tcp_server_spec.rb +158 -0
  47. metadata +206 -0
@@ -0,0 +1,55 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'../lib')
2
+
3
+ require 'rmodbus'
4
+ require 'benchmark'
5
+
6
+ include ModBus
7
+
8
+ TIMES = 1000
9
+
10
+ srv = ModBus::TCPServer.new 1502
11
+ srv.coils = [0,1] * 50
12
+ srv.discrete_inputs = [1,0] * 50
13
+ srv.holding_registers = [0,1,2,3,4,5,6,7,8,9] * 10
14
+ srv.input_registers = [0,1,2,3,4,5,6,7,8,9] * 10
15
+ srv.start
16
+
17
+
18
+ cl = TCPClient.new('127.0.0.1', 1502)
19
+ cl.with_slave(1) do |slave|
20
+ Benchmark.bmbm do |x|
21
+ x.report('Read coils') do
22
+ TIMES.times { slave.read_coils 0, 100 }
23
+ end
24
+
25
+ x.report('Read discrete inputs') do
26
+ TIMES.times { slave.read_discrete_inputs 0, 100 }
27
+ end
28
+
29
+ x.report('Read holding registers') do
30
+ TIMES.times { slave.read_holding_registers 0, 100 }
31
+ end
32
+
33
+ x.report('Read input registers') do
34
+ TIMES.times { slave.read_input_registers 0, 100 }
35
+ end
36
+
37
+ x.report('Write single coil') do
38
+ TIMES.times { slave.write_single_coil 0, 1 }
39
+ end
40
+
41
+ x.report('Write single register') do
42
+ TIMES.times { slave.write_single_register 100, 0xAAAA }
43
+ end
44
+
45
+ x.report('Write multiple coils') do
46
+ TIMES.times { slave.write_multiple_coils 0, [1,0] * 50 }
47
+ end
48
+
49
+ x.report('Write multiple registers') do
50
+ TIMES.times { slave.write_multiple_registers 0, [0,1,2,3,4,5,6,7,8,9] * 10 }
51
+ end
52
+ end
53
+ end
54
+ cl.close
55
+ srv.stop
@@ -0,0 +1,84 @@
1
+ =begin
2
+ It's very simple example of implementation XPCA gateway (see http://www.xpca.org)
3
+ for communication with TCP ModBus devices
4
+ It receives REST requests (e.g http://127.0.0.1:4567/mb/127.0.0.1/8502/1/coils/6/17 )
5
+ and returns data in JSON format addr : data:
6
+ {"coils": {
7
+ "6":{
8
+ "value":0,
9
+ "timestamp":"2011-07-12 18:11:03 +0000",
10
+ "quality":"good"
11
+ },
12
+ "7":{
13
+ "value":0,
14
+ "timestamp":"2011-07-12 18:11:03 +0000",
15
+ "quality":"good"
16
+ }
17
+ ...
18
+ }
19
+
20
+ This code requies gems: rmodbus, sinatra and json
21
+ 2011 (c) Aleksey Timin
22
+ =end
23
+
24
+ require 'rubygems'
25
+ require 'rmodbus'
26
+ require 'sinatra'
27
+ require 'json'
28
+
29
+ # Launche TCP ModBus server for test
30
+ IP = '127.0.0.1'
31
+ PORT = 8502
32
+
33
+ @srv = ModBus::TCPServer.new(PORT,1)
34
+
35
+ @srv.holding_registers = Array.new(100) { |i| i = i + 1 }
36
+ @srv.input_registers = Array.new(100) { |i| i = i + 1 }
37
+ @srv.coils = Array.new(100) { |i| i = 0 }
38
+ @srv.discrete_inputs = Array.new(100) { |i| i = 0 }
39
+
40
+ @srv.start
41
+
42
+
43
+ # Calc a GET request
44
+ # @example
45
+ # http://127.0.0.1:4567/mb/127.0.0.1/8502/1/coils/6/17
46
+ #
47
+ # HTTP route: GET http://localhost/mb/:ip/:port/:slave/:dataplace/:firstaddr/:lastaddr
48
+ #
49
+ # :ip - ip addr of ModBus TCP Server
50
+ # :port - port of ModBUs TCP Server
51
+ # :slave - id of slave device
52
+ # :dataplace - valid values: coils, discrete_inputs, input_registers, holding_registers
53
+ # :firstaddr - first addr of registers(contacts)
54
+ # :lastaddr - last addr of registers(contacts)
55
+ get '/mb/:ip/:port/:slave/:dataplace/:firstaddr/:lastaddr' do
56
+ resp = {}
57
+ begin
58
+ data = []
59
+ ModBus::TCPClient.new(params[:ip].to_s, params[:port].to_i) do |cl|
60
+ cl.with_slave(params[:slave].to_i) do |slave|
61
+ slave.debug = true
62
+ dataplace = slave.send params[:dataplace]
63
+ data = dataplace[params[:firstaddr].to_i .. params[:lastaddr].to_i]
64
+ end
65
+ end
66
+
67
+ resp = { params[:dataplace] => {}}
68
+ data.each_with_index do |v,i|
69
+ resp[params[:dataplace]][params[:firstaddr].to_i + i] = {
70
+ :value => v,
71
+ :timestamp => Time.now.utc.strftime("%Y-%m-%d %H:%M:%S %z"),
72
+ :quality => "good"
73
+ }
74
+ end
75
+ rescue Exception => e
76
+ resp = { :error => {
77
+ :type => e.class,
78
+ :message => e.message }
79
+ }
80
+ end
81
+
82
+ content_type "application/json"
83
+ resp.to_json
84
+ end
@@ -0,0 +1,22 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib/')
2
+ require 'rmodbus'
3
+
4
+ srv = ModBus::RTUViaTCPServer.new(10002,1)
5
+ srv.coils = [1,0,1,1]
6
+ srv.discrete_inputs = [1,1,0,0]
7
+ srv.holding_registers = [1,2,3,4]
8
+ srv.input_registers = [1,2,3,4]
9
+ srv.debug = true
10
+ srv.start
11
+
12
+ ModBus::RTUClient.connect('127.0.0.1', 10002) do |cl|
13
+ cl.with_slave(1) do |slave|
14
+ slave.debug = true
15
+ regs = slave.holding_registers
16
+ puts regs[0..3]
17
+ regs[0..3] = [2,0,1,1]
18
+ puts regs[0..3]
19
+ end
20
+ end
21
+
22
+ srv.shutdown
@@ -0,0 +1,23 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib/')
2
+ require 'rmodbus'
3
+
4
+ srv = ModBus::TCPServer.new(8502,1)
5
+ srv.coils = [1,0,1,1]
6
+ srv.discrete_inputs = [1,1,0,0]
7
+ srv.holding_registers = [1,2,3,4]
8
+ srv.input_registers = [1,2,3,4]
9
+ srv.debug = true
10
+ srv.audit = true
11
+ srv.start
12
+
13
+ ModBus::TCPClient.connect('127.0.0.1', 8502) do |cl|
14
+ cl.with_slave(1) do |slave|
15
+ slave.debug = true
16
+ regs = slave.holding_registers
17
+ puts regs[0..3]
18
+ regs[0..3] = [2,0,1,1]
19
+ puts regs[0..3]
20
+ end
21
+ end
22
+
23
+ srv.stop
data/lib/rmodbus.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'rmodbus/ext'
2
+ require 'rmodbus/proxy'
3
+ require 'rmodbus/version'
4
+
5
+ module ModBus
6
+ autoload :Errors, 'rmodbus/errors'
7
+ autoload :Debug, 'rmodbus/debug'
8
+ autoload :Options, 'rmodbus/options'
9
+ autoload :SP, 'rmodbus/sp'
10
+ autoload :RTU, 'rmodbus/rtu'
11
+ autoload :TCP, 'rmodbus/tcp'
12
+ autoload :Client, 'rmodbus/client'
13
+ autoload :Server, 'rmodbus/server'
14
+ autoload :TCPSlave, 'rmodbus/tcp_slave'
15
+ autoload :TCPClient, 'rmodbus/tcp_client'
16
+ autoload :TCPServer, 'rmodbus/tcp_server'
17
+ autoload :RTUSlave, 'rmodbus/rtu_slave'
18
+ autoload :RTUClient, 'rmodbus/rtu_client'
19
+ autoload :RTUServer, 'rmodbus/rtu_server'
20
+ autoload :RTUViaTCPServer, 'rmodbus/rtu_via_tcp_server'
21
+ end
@@ -0,0 +1,94 @@
1
+ module ModBus
2
+ # @abstract
3
+ class Client
4
+ autoload :Slave, 'rmodbus/client/slave'
5
+
6
+ include Errors
7
+ include Debug
8
+ include Options
9
+
10
+ # Initialized client (alias :connect)
11
+ # @example
12
+ # Client.new(any_args) do |client|
13
+ # client.closed? #=> false
14
+ # end
15
+ # @param *args depends on implementation
16
+ # @yield return client object and close it before exit
17
+ # @return [Client] client object
18
+ def initialize(*args, &block)
19
+ # Defaults
20
+ @debug = false
21
+ @raise_exception_on_mismatch = false
22
+ @read_retry_timeout = 1
23
+ @read_retries = 1
24
+
25
+ @io = open_connection(*args)
26
+ if block_given?
27
+ begin
28
+ yield self
29
+ ensure
30
+ close
31
+ end
32
+ else
33
+ self
34
+ end
35
+ end
36
+
37
+ class << self
38
+ alias_method :connect, :new
39
+ end
40
+
41
+ # Given slave object
42
+ # @example
43
+ # cl = Client.new
44
+ # cl.with_slave(1) do |slave|
45
+ # slave.holding_registers[0..100]
46
+ # end
47
+ #
48
+ # @param [Integer, #read] uid slave devise
49
+ # @return [Slave] slave object
50
+ def with_slave(uid, &block)
51
+ slave = get_slave(uid, @io)
52
+ slave.debug = debug
53
+ slave.raise_exception_on_mismatch = raise_exception_on_mismatch
54
+ slave.read_retries = read_retries
55
+ slave.read_retry_timeout = read_retry_timeout
56
+ if block_given?
57
+ yield slave
58
+ else
59
+ slave
60
+ end
61
+ end
62
+
63
+ # Check connections
64
+ # @return [Boolean]
65
+ def closed?
66
+ @io.closed?
67
+ end
68
+
69
+ # Close connections
70
+ def close
71
+ @io.close unless @io.closed?
72
+ end
73
+
74
+ protected
75
+ def open_connection(*args)
76
+ #Stub conn object
77
+ @io = Object.new
78
+
79
+ @io.instance_eval """
80
+ def close
81
+ end
82
+
83
+ def closed?
84
+ true
85
+ end
86
+ """
87
+ @io
88
+ end
89
+
90
+ def get_slave(uid,io)
91
+ Slave.new(uid, io)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,345 @@
1
+ require 'timeout'
2
+
3
+ module ModBus
4
+ class Client
5
+ class Slave
6
+ include Errors
7
+ include Debug
8
+ include Options
9
+ # Number of times to retry on read and read timeouts
10
+ 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
+ }
20
+ def initialize(uid, io)
21
+ @uid = uid
22
+ @io = io
23
+ end
24
+
25
+ # Returns a ModBus::ReadWriteProxy hash interface for coils
26
+ #
27
+ # @example
28
+ # coils[addr] => [1]
29
+ # coils[addr1..addr2] => [1, 0, ..]
30
+ # coils[addr] = 0 => [0]
31
+ # coils[addr1..addr2] = [1, 0, ..] => [1, 0, ..]
32
+ #
33
+ # @return [ReadWriteProxy] proxy object
34
+ def coils
35
+ ModBus::ReadWriteProxy.new(self, :coil)
36
+ end
37
+
38
+ # Read coils
39
+ #
40
+ # @example
41
+ # read_coils(addr, ncoils) => [1, 0, ..]
42
+ #
43
+ # @param [Integer] addr address first coil
44
+ # @param [Integer] ncoils number coils
45
+ # @return [Array] coils
46
+ def read_coils(addr, ncoils = 1)
47
+ query("\x1" + addr.to_word + ncoils.to_word).unpack_bits[0..ncoils-1]
48
+ end
49
+
50
+ def read_coil(addr)
51
+ read_coils(addr, 1).first
52
+ end
53
+
54
+ # Write a single coil
55
+ #
56
+ # @example
57
+ # write_single_coil(1, 0) => self
58
+ #
59
+ # @param [Integer] addr address coil
60
+ # @param [Integer] val value coil (0 or other)
61
+ # @return self
62
+ def write_single_coil(addr, val)
63
+ if val == 0
64
+ query("\x5" + addr.to_word + 0.to_word)
65
+ else
66
+ query("\x5" + addr.to_word + 0xff00.to_word)
67
+ end
68
+ self
69
+ end
70
+ alias_method :write_coil, :write_single_coil
71
+
72
+ # Write multiple coils
73
+ #
74
+ # @example
75
+ # write_multiple_coils(1, [0,1,0,1]) => self
76
+ #
77
+ # @param [Integer] addr address first coil
78
+ # @param [Array] vals written coils
79
+ def write_multiple_coils(addr, vals)
80
+ nbyte = ((vals.size-1) >> 3) + 1
81
+ sum = 0
82
+ (vals.size - 1).downto(0) do |i|
83
+ sum = sum << 1
84
+ sum |= 1 if vals[i] > 0
85
+ end
86
+
87
+ s_val = ""
88
+ nbyte.times do
89
+ s_val << (sum & 0xff).chr
90
+ sum >>= 8
91
+ end
92
+
93
+ query("\xf" + addr.to_word + vals.size.to_word + nbyte.chr + s_val)
94
+ self
95
+ end
96
+ alias_method :write_coils, :write_multiple_coils
97
+
98
+ # Returns a ModBus::ReadOnlyProxy hash interface for discrete inputs
99
+ #
100
+ # @example
101
+ # discrete_inputs[addr] => [1]
102
+ # discrete_inputs[addr1..addr2] => [1, 0, ..]
103
+ #
104
+ # @return [ReadOnlyProxy] proxy object
105
+ def discrete_inputs
106
+ ModBus::ReadOnlyProxy.new(self, :discrete_input)
107
+ end
108
+
109
+ # Read discrete inputs
110
+ #
111
+ # @example
112
+ # read_discrete_inputs(addr, ninputs) => [1, 0, ..]
113
+ #
114
+ # @param [Integer] addr address first input
115
+ # @param[Integer] ninputs number inputs
116
+ # @return [Array] inputs
117
+ def read_discrete_inputs(addr, ninputs = 1)
118
+ query("\x2" + addr.to_word + ninputs.to_word).unpack_bits[0..ninputs-1]
119
+ end
120
+
121
+ def read_discrete_input(addr)
122
+ read_discrete_inputs(addr, 1).first
123
+ end
124
+
125
+ # Returns a read/write ModBus::ReadOnlyProxy hash interface for coils
126
+ #
127
+ # @example
128
+ # input_registers[addr] => [1]
129
+ # input_registers[addr1..addr2] => [1, 0, ..]
130
+ #
131
+ # @return [ReadOnlyProxy] proxy object
132
+ def input_registers
133
+ ModBus::ReadOnlyProxy.new(self, :input_register)
134
+ end
135
+
136
+ # Read input registers
137
+ #
138
+ # @example
139
+ # read_input_registers(1, 5) => [1, 0, ..]
140
+ #
141
+ # @param [Integer] addr address first registers
142
+ # @param [Integer] nregs number registers
143
+ # @return [Array] registers
144
+ def read_input_registers(addr, nregs = 1)
145
+ query("\x4" + addr.to_word + nregs.to_word).unpack('n*')
146
+ end
147
+
148
+ def read_input_register(addr)
149
+ read_input_registers(addr, 1).first
150
+ end
151
+
152
+ # Returns a ModBus::ReadWriteProxy hash interface for holding registers
153
+ #
154
+ # @example
155
+ # holding_registers[addr] => [123]
156
+ # holding_registers[addr1..addr2] => [123, 234, ..]
157
+ # holding_registers[addr] = 123 => 123
158
+ # holding_registers[addr1..addr2] = [234, 345, ..] => [234, 345, ..]
159
+ #
160
+ # @return [ReadWriteProxy] proxy object
161
+ def holding_registers
162
+ ModBus::ReadWriteProxy.new(self, :holding_register)
163
+ end
164
+
165
+ # Read holding registers
166
+ #
167
+ # @example
168
+ # read_holding_registers(1, 5) => [1, 0, ..]
169
+ #
170
+ # @param [Integer] addr address first registers
171
+ # @param [Integer] nregs number registers
172
+ # @return [Array] registers
173
+ def read_holding_registers(addr, nregs = 1)
174
+ query("\x3" + addr.to_word + nregs.to_word).unpack('n*')
175
+ end
176
+
177
+ def read_holding_register(addr)
178
+ read_holding_registers(addr, 1).first
179
+ end
180
+
181
+ # Write a single holding register
182
+ #
183
+ # @example
184
+ # write_single_register(1, 0xaa) => self
185
+ #
186
+ # @param [Integer] addr address registers
187
+ # @param [Integer] val written to register
188
+ # @return self
189
+ def write_single_register(addr, val)
190
+ query("\x6" + addr.to_word + val.to_word)
191
+ self
192
+ end
193
+ alias_method :write_holding_register, :write_single_register
194
+
195
+
196
+ # Write multiple holding registers
197
+ #
198
+ # @example
199
+ # write_multiple_registers(1, [0xaa, 0]) => self
200
+ #
201
+ # @param [Integer] addr address first registers
202
+ # @param [Array] val written registers
203
+ # @return self
204
+ def write_multiple_registers(addr, vals)
205
+ s_val = ""
206
+ vals.each do |reg|
207
+ s_val << reg.to_word
208
+ end
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 = ""
239
+ vals.each do |reg|
240
+ s_val << reg.to_word
241
+ end
242
+
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*')
245
+ end
246
+ alias_method :read_write_holding_registers, :read_write_multiple_registers
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 == 0
270
+ end
271
+ rescue ModBusTimeout => err
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.size == 0
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
+
292
+ private
293
+ def check_response_mismatch(request, response)
294
+ read_func = response.getbyte(0)
295
+ data = response[2..-1]
296
+ #Mismatch functional code
297
+ 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
301
+
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
309
+ 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
314
+ exp_addr = request.getword(1)
315
+ got_addr = response.getword(1)
316
+ if exp_addr != got_addr
317
+ msg = "Address is mismatch (expected #{exp_addr}, got #{got_addr})"
318
+ end
319
+
320
+ exp_val = request.getword(3)
321
+ 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
326
+ exp_addr = request.getword(1)
327
+ got_addr = response.getword(1)
328
+ if exp_addr != got_addr
329
+ msg = "Address is mismatch (expected #{exp_addr}, got #{got_addr})"
330
+ end
331
+
332
+ exp_quant = request.getword(3)
333
+ got_quant = response.getword(3)
334
+ if exp_quant != got_quant
335
+ msg = "Quantity is mismatch (expected #{exp_quant}, got #{got_quant})"
336
+ end
337
+ else
338
+ warn "Fuiction (#{read_func}) is not supported raising response mismatch"
339
+ end
340
+
341
+ raise ResponseMismatch.new(msg, request, response) if msg
342
+ end
343
+ end
344
+ end
345
+ end