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
@@ -1,127 +1,270 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModBus
2
4
  # Module for implementation ModBus server
3
5
  module Server
4
- Funcs = [1,2,3,4,5,6,15,16]
6
+ autoload :Slave, "rmodbus/server/slave"
7
+
8
+ attr_accessor :promiscuous, :request_callback, :response_callback
5
9
 
6
- attr_accessor :coils, :discrete_inputs, :holding_registers, :input_registers, :uid
7
- @coils = []
8
- @discrete_inputs = []
9
- @holding_registers =[]
10
- @input_registers = []
10
+ FUNCS = [1, 2, 3, 4, 5, 6, 15, 16, 22, 23].freeze
11
+
12
+ def with_slave(uid)
13
+ slave = slaves[uid] ||= Server::Slave.new
14
+ if block_given?
15
+ yield slave
16
+ else
17
+ slave
18
+ end
19
+ end
11
20
 
12
21
  private
13
22
 
14
- def exec_req(req, coils, discrete_inputs, holding_registers, input_registers)
15
- func = req.getbyte(0)
23
+ def slaves
24
+ @slaves ||= {}
25
+ end
26
+
27
+ def exec_req(uid, func, params, pdu, is_response: false)
28
+ if is_response
29
+ log("Server RX response #{func & 0x7f} from #{uid}: #{params.inspect}")
30
+ else
31
+ log("Server RX function #{func} to #{uid}: #{params.inspect}")
32
+ end
33
+ request_callback&.call(uid, func, params) unless is_response
34
+
35
+ if uid.zero?
36
+ slaves.each_key { |specific_uid| exec_req(specific_uid, func, params, pdu) }
37
+ return
38
+ end
39
+ slave = slaves[uid]
40
+ return nil if !slave && !promiscuous
41
+
42
+ if promiscuous && !slave && is_response
43
+ # we saw a request to a slave that we don't own; try
44
+ # and parse this as a response, not a request
45
+
46
+ response_callback&.call(uid, func, params, @pending_response_req)
47
+ @pending_response_req = nil
48
+ return
49
+ end
50
+
51
+ unless FUNCS.include?(func)
52
+ log("Server RX unrecognized function #{func} to #{uid}")
53
+ return unless slave
16
54
 
17
- unless Funcs.include?(func)
18
- params = { :err => 1 }
55
+ return (func | 0x80).chr + 1.chr
19
56
  end
20
57
 
58
+ # keep track of the request so that promiscuous printing of the response can have context if necessary
59
+ @pending_response_req = params
60
+
61
+ return unless slave
62
+
63
+ pdu = process_func(func, slave, pdu, params)
64
+ if response_callback
65
+ res = parse_response(pdu.getbyte(0), pdu)
66
+ response_callback.call(uid, pdu.getbyte(0), res, params)
67
+ end
68
+ pdu
69
+ end
70
+
71
+ def parse_request(func, req)
21
72
  case func
22
- when 1
23
- params = parse_read_func(req, coils, 2000)
24
- if params[:err] == 0
25
- val = coils[params[:addr],params[:quant]].pack_to_word
26
- pdu = func.chr + val.size.chr + val
27
- end
28
- when 2
29
- params = parse_read_func(req, discrete_inputs, 2000)
30
- if params[:err] == 0
31
- val = discrete_inputs[params[:addr],params[:quant]].pack_to_word
32
- pdu = func.chr + val.size.chr + val
33
- end
34
- when 3
35
- params = parse_read_func(req, holding_registers)
36
- if params[:err] == 0
37
- pdu = func.chr + (params[:quant] * 2).chr + holding_registers[params[:addr],params[:quant]].pack('n*')
38
- end
39
- when 4
40
- params = parse_read_func(req, input_registers)
41
- if params[:err] == 0
42
- pdu = func.chr + (params[:quant] * 2).chr + input_registers[params[:addr],params[:quant]].pack('n*')
43
- end
44
- when 5
45
- params = parse_write_coil_func(req)
46
- if params[:err] == 0
47
- coils[params[:addr]] = params[:val]
48
- pdu = req
49
- end
50
- when 6
51
- params = parse_write_register_func(req)
52
- if params[:err] == 0
53
- holding_registers[params[:addr]] = params[:val]
54
- pdu = req
55
- end
56
- when 15
57
- params = parse_write_multiple_coils_func(req)
58
- if params[:err] == 0
59
- coils[params[:addr],params[:quant]] = params[:val][0,params[:quant]]
60
- pdu = req[0,5]
61
- end
62
- when 16
63
- params = parse_write_multiple_registers_func(req)
64
- if params[:err] == 0
65
- holding_registers[params[:addr],params[:quant]] = params[:val][0,params[:quant]]
66
- pdu = req[0,5]
67
- end
73
+ when 1, 2, 3, 4
74
+ parse_read_func(req)
75
+ when 5
76
+ parse_write_coil_func(req)
77
+ when 6
78
+ parse_write_register_func(req)
79
+ when 15
80
+ parse_write_multiple_coils_func(req)
81
+ when 16
82
+ parse_write_multiple_registers_func(req)
83
+ when 22
84
+ parse_mask_write_register_func(req)
85
+ when 23
86
+ parse_read_write_multiple_registers_func(req)
68
87
  end
88
+ end
69
89
 
70
- if params[:err] == 0
71
- pdu
90
+ def parse_response(func, res)
91
+ if func & 0x80 == 0x80 && FUNCS.include?(func & 0x7f)
92
+ return nil unless res.length == 2
93
+
94
+ return { err: res[1].ord }
95
+ end
96
+
97
+ case func
98
+ when 1, 2
99
+ return nil unless res.length == res[1].ord + 2
100
+
101
+ res[2..].unpack_bits
102
+ when 3, 4, 23
103
+ return nil unless res.length == res[1].ord + 2
104
+
105
+ res[2..].unpack("n*")
106
+ when 5, 6, 15, 16
107
+ return nil unless res.length == 5
108
+
109
+ {}
110
+ when 22
111
+ return nil unless res.length == 7
112
+
113
+ {}
114
+ end
115
+ end
116
+
117
+ def process_func(func, slave, req, params)
118
+ case func
119
+ when 1
120
+ unless (err = validate_read_func(params, slave.coils, 2000))
121
+ val = slave.coils[params[:addr], params[:quant]].pack_bits
122
+ pdu = func.chr + val.size.chr + val
123
+ end
124
+ when 2
125
+ unless (err = validate_read_func(params, slave.discrete_inputs, 2000))
126
+ val = slave.discrete_inputs[params[:addr], params[:quant]].pack_bits
127
+ pdu = func.chr + val.size.chr + val
128
+ end
129
+ when 3
130
+ unless (err = validate_read_func(params, slave.holding_registers))
131
+ pdu = func.chr + (params[:quant] * 2).chr + slave.holding_registers[params[:addr],
132
+ params[:quant]].pack("n*")
133
+ end
134
+ when 4
135
+ unless (err = validate_read_func(params, slave.input_registers))
136
+ pdu = func.chr + (params[:quant] * 2).chr + slave.input_registers[params[:addr], params[:quant]].pack("n*")
137
+ end
138
+ when 5
139
+ unless (err = validate_write_coil_func(params, slave))
140
+ params[:val] = 1 if params[:val] == 0xff00
141
+ slave.coils[params[:addr]] = params[:val]
142
+ pdu = req
143
+ end
144
+ when 6
145
+ unless (err = validate_write_register_func(params, slave))
146
+ slave.holding_registers[params[:addr]] = params[:val]
147
+ pdu = req
148
+ end
149
+ when 15
150
+ unless (err = validate_write_multiple_coils_func(params, slave))
151
+ slave.coils[params[:addr], params[:quant]] = params[:val][0, params[:quant]]
152
+ pdu = req[0, 5]
153
+ end
154
+ when 16
155
+ unless (err = validate_write_multiple_registers_func(params, slave))
156
+ slave.holding_registers[params[:addr], params[:quant]] = params[:val]
157
+ pdu = req[0, 5]
158
+ end
159
+ when 22
160
+ unless (err = validate_write_register_func(params, slave))
161
+ addr = params[:addr]
162
+ and_mask = params[:and_mask]
163
+ slave.holding_registers[addr] = (slave.holding_registers[addr] & and_mask) | (params[:or_mask] & ~and_mask)
164
+ pdu = req
165
+ end
166
+ when 23
167
+ unless (err = validate_read_write_multiple_registers_func(params, slave))
168
+ slave.holding_registers[params[:write][:addr], params[:write][:quant]] = params[:write][:val]
169
+ pdu = func.chr + (params[:read][:quant] * 2).chr + slave.holding_registers[params[:read][:addr],
170
+ params[:read][:quant]].pack("n*")
171
+ end
172
+ end
173
+
174
+ if err
175
+ (func | 0x80).chr + err.chr
72
176
  else
73
- pdu = (func | 0x80).chr + params[:err].chr
177
+ pdu
74
178
  end
75
179
  end
76
180
 
77
- def parse_read_func(req, field, quant_max=0x7d)
78
- quant = req[3,2].unpack('n')[0]
181
+ def parse_read_func(req, expected_length = 5)
182
+ return nil if expected_length && req.length != expected_length
79
183
 
80
- return { :err => 3} unless quant <= quant_max
184
+ { quant: req[3, 2].unpack1("n"), addr: req[1, 2].unpack1("n") }
185
+ end
81
186
 
82
- addr = req[1,2].unpack('n')[0]
83
- return { :err => 2 } unless addr + quant <= field.size
187
+ def validate_read_func(params, field, quant_max = 0x7d)
188
+ return 3 unless params[:quant] <= quant_max
84
189
 
85
- return { :err => 0, :quant => quant, :addr => addr }
190
+ 2 unless params[:addr] + params[:quant] <= field.size
86
191
  end
87
192
 
88
193
  def parse_write_coil_func(req)
89
- addr = req[1,2].unpack('n')[0]
90
- return { :err => 2 } unless addr <= @coils.size
194
+ return nil unless req.length == 5
91
195
 
92
- val = req[3,2].unpack('n')[0]
93
- return { :err => 3 } unless val == 0 or val == 0xff00
196
+ { addr: req[1, 2].unpack1("n"), val: req[3, 2].unpack1("n") }
197
+ end
198
+
199
+ def validate_write_coil_func(params, slave)
200
+ return 2 unless params[:addr] <= slave.coils.size
94
201
 
95
- val = 1 if val == 0xff00
96
- return { :err => 0, :addr => addr, :val => val }
202
+ 3 unless params[:val].zero? || (params[:val] == 0xff00)
97
203
  end
98
204
 
99
205
  def parse_write_register_func(req)
100
- addr = req[1,2].unpack('n')[0]
101
- return { :err => 2 } unless addr <= @holding_registers.size
206
+ return nil unless req.length == 5
102
207
 
103
- val = req[3,2].unpack('n')[0]
208
+ { addr: req[1, 2].unpack1("n"), val: req[3, 2].unpack1("n") }
209
+ end
104
210
 
105
- return { :err => 0, :addr => addr, :val => val }
106
- end
211
+ def validate_write_register_func(params, slave)
212
+ 2 unless params[:addr] <= slave.holding_registers.size
213
+ end
107
214
 
108
215
  def parse_write_multiple_coils_func(req)
109
- params = parse_read_func(req, @coils)
216
+ return nil if req.length < 7
110
217
 
111
- if params[:err] == 0
112
- params = {:err => 0, :addr => params[:addr], :quant => params[:quant], :val => req[6,params[:quant]].unpack_bits }
113
- end
218
+ params = parse_read_func(req, nil)
219
+ return nil if req.length != 6 + ((params[:quant] + 7) / 8)
220
+
221
+ params[:val] = req[6, params[:quant]].unpack_bits
114
222
  params
115
223
  end
116
224
 
225
+ def validate_write_multiple_coils_func(params, slave)
226
+ validate_read_func(params, slave.coils)
227
+ end
228
+
117
229
  def parse_write_multiple_registers_func(req)
118
- params = parse_read_func(req, @holding_registers)
230
+ return nil if req.length < 8
119
231
 
120
- if params[:err] == 0
121
- params = {:err => 0, :addr => params[:addr], :quant => params[:quant], :val => req[6,params[:quant] * 2].unpack('n*')}
122
- end
232
+ params = parse_read_func(req, nil)
233
+ return nil if req.length != 6 + (params[:quant] * 2)
234
+
235
+ params[:val] = req[6, params[:quant] * 2].unpack("n*")
123
236
  params
124
237
  end
125
238
 
239
+ def validate_write_multiple_registers_func(params, slave)
240
+ validate_read_func(params, slave.holding_registers)
241
+ end
242
+
243
+ def parse_mask_write_register_func(req)
244
+ return nil if req.length != 7
245
+
246
+ {
247
+ addr: req[1, 2].unpack1("n"),
248
+ and_mask: req[3, 2].unpack1("n"),
249
+ or_mask: req[5, 2].unpack1("n")
250
+ }
251
+ end
252
+
253
+ def parse_read_write_multiple_registers_func(req)
254
+ return nil if req.length < 12
255
+
256
+ params = { read: parse_read_func(req, nil),
257
+ write: parse_write_multiple_registers_func(req[4..]) }
258
+ return nil if params[:write].nil?
259
+
260
+ params
261
+ end
262
+
263
+ def validate_read_write_multiple_registers_func(params, slave)
264
+ result = validate_read_func(params[:read], slave.holding_registers)
265
+ return result if result
266
+
267
+ validate_write_multiple_registers_func(params[:write], slave)
268
+ end
126
269
  end
127
270
  end
data/lib/rmodbus/sp.rb CHANGED
@@ -1,36 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
- require 'serialport'
3
- rescue Exception => e
4
- warn "[WARNING] Install `serialport` gem for use RTU protocols"
4
+ require "ccutrer-serialport"
5
+ rescue LoadError
6
+ warn "[WARNING] Install `ccutrer-serialport` gem for use RTU protocols"
5
7
  end
6
8
 
7
9
  module ModBus
8
10
  module SP
9
11
  attr_reader :port, :baud, :data_bits, :stop_bits, :parity, :read_timeout
12
+
10
13
  # Open serial port
11
- # @param [String] port name serial ports ("/dev/ttyS0" POSIX, "com1" - Windows)
14
+ # @param [String] port name serial ports ("/dev/ttyS0")
12
15
  # @param [Integer] baud rate serial port (default 9600)
13
16
  # @param [Hash] opts the options of serial port
14
17
  #
15
18
  # @option opts [Integer] :data_bits from 5 to 8
16
19
  # @option opts [Integer] :stop_bits 1 or 2
17
- # @option opts [Integer] :parity NONE, EVEN or ODD
18
- # @option opts [Integer] :read_timeout default 100 ms
20
+ # @option opts [Integer] :parity :none, :even or :odd
19
21
  # @return [SerialPort] io serial port
20
22
  def open_serial_port(port, baud, opts = {})
21
23
  @port, @baud = port, baud
22
24
 
23
- @data_bits, @stop_bits, @parity, @read_timeout = 8, 1, SerialPort::NONE, 100
25
+ @data_bits, @stop_bits, @parity = 8, 1, :none
24
26
 
25
27
  @data_bits = opts[:data_bits] unless opts[:data_bits].nil?
26
28
  @stop_bits = opts[:stop_bits] unless opts[:stop_bits].nil?
27
29
  @parity = opts[:parity] unless opts[:parity].nil?
28
- @read_timeout = opts[:read_timeout] unless opts[:read_timeout].nil?
29
30
 
30
- io = SerialPort.new(@port, @baud, @data_bits, @stop_bits, @parity)
31
- io.flow_control = SerialPort::NONE
32
- io.read_timeout = @read_timeout
33
- io
31
+ CCutrer::SerialPort.new(@port, baud: @baud, data_bits: @data_bits, stop_bits: @stop_bits, parity: @parity)
34
32
  end
35
33
  end
36
34
  end
data/lib/rmodbus/tcp.rb CHANGED
@@ -1,32 +1,31 @@
1
- require 'socket'
2
- require 'timeout'
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
3
4
 
4
5
  module ModBus
5
6
  module TCP
6
7
  include Errors
7
- include Timeout
8
8
  attr_reader :ipaddr, :port
9
+
9
10
  # Open TCP socket
10
11
  #
11
12
  # @param [String] ipaddr IP address of remote server
12
13
  # @param [Integer] port connection port
13
14
  # @param [Hash] opts options of connection
14
15
  # @option opts [Float, Integer] :connect_timeout seconds timeout for open socket
15
- # @return [TCPSocket] socket
16
+ # @return [Socket] socket
16
17
  #
17
18
  # @raise [ModBusTimeout] timed out attempting to create connection
18
19
  def open_tcp_connection(ipaddr, port, opts = {})
19
20
  @ipaddr, @port = ipaddr, port
20
21
 
21
- opts[:connect_timeout] ||= 1
22
+ timeout = opts[:connect_timeout] ||= 1
22
23
 
23
24
  io = nil
24
25
  begin
25
- timeout(opts[:connect_timeout], ModBusTimeout) do
26
- io = TCPSocket.new(@ipaddr, @port)
27
- end
28
- rescue ModBusTimeout => err
29
- raise ModBusTimeout.new, 'Timed out attempting to create connection'
26
+ io = Socket.tcp(@ipaddr, @port, nil, nil, connect_timeout: timeout)
27
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
28
+ raise ModBusTimeout.new, "Timed out attempting to create connection"
30
29
  end
31
30
 
32
31
  io
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModBus
2
4
  # TCP client implementation
3
5
  # @example
@@ -13,6 +15,7 @@ module ModBus
13
15
  include TCP
14
16
 
15
17
  protected
18
+
16
19
  # Open TCP\IP connection
17
20
  def open_connection(ipaddr, port = 502, opts = {})
18
21
  open_tcp_connection(ipaddr, port, opts)
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
- require 'gserver'
4
+ require "gserver"
3
5
  rescue
4
6
  warn "[WARNING] Install `gserver` gem for use TCPServer"
5
7
  end
@@ -7,16 +9,17 @@ end
7
9
  module ModBus
8
10
  # TCP server implementation
9
11
  # @example
10
- # srv = TCPServer.new(10002, 1)
11
- # srv.coils = [1,0,1,1]
12
- # srv.discrete_inputs = [1,1,0,0]
13
- # srv.holding_registers = [1,2,3,4]
14
- # srv.input_registers = [1,2,3,4]
15
- # srv.debug = true
12
+ # srv = TCPServer.new(10002)
13
+ # slave = srv.with_slave(255)
14
+ # slave.coils = [1,0,1,1]
15
+ # slave.discrete_inputs = [1,1,0,0]
16
+ # slave.holding_registers = [1,2,3,4]
17
+ # slave.input_registers = [1,2,3,4]
18
+ # srv.logger = Logger.new($stdout)
16
19
  # srv.start
17
- class TCPServer < GServer
18
- include Debug
19
- include Server
20
+ class TCPServer < GServer
21
+ include Debug
22
+ include Server
20
23
 
21
24
  # Init server
22
25
  # @param [Integer] port listen port
@@ -24,40 +27,43 @@ module ModBus
24
27
  # @param [Hash] opts options of server
25
28
  # @option opts [String] :host host of server default '127.0.0.1'
26
29
  # @option opts [Float, Integer] :max_connection max of TCP connection with server default 4
27
- def initialize(port = 502, uid = 1, opts = {})
28
- @uid = uid
29
-
30
- warn "[WARNING] Please, use UID = 255. It will be fixed in the next release." if @uid != 0xff
31
-
30
+ def initialize(port = 502, opts = {})
32
31
  opts[:host] = DEFAULT_HOST unless opts[:host]
33
32
  opts[:max_connection] = 4 unless opts[:max_connection]
34
- super(port, host = opts[:host], maxConnection = opts[:max_connection])
35
- end
33
+ super(port, opts[:host], opts[:max_connection])
34
+ end
35
+
36
+ # set the default param
37
+ def with_slave(uid = 255)
38
+ super
39
+ end
36
40
 
37
41
  # Serve requests
38
42
  # @param [TCPSocket] io socket
39
43
  def serve(io)
40
- while not stopped?
44
+ until stopped?
41
45
  header = io.read(7)
42
- tx_id = header[0,2]
43
- proto_id = header[2,2]
44
- len = header[4,2].unpack('n')[0]
46
+ tx_id = header[0, 2]
47
+ proto_id = header[2, 2]
48
+ len = header[4, 2].unpack1("n")
45
49
  unit_id = header.getbyte(6)
46
- if proto_id == "\x00\x00"
47
- req = io.read(len - 1)
48
- if unit_id == @uid || unit_id == 0
49
- log "Server RX (#{req.size} bytes): #{logging_bytes(req)}"
50
-
51
- pdu = exec_req(req, @coils, @discrete_inputs, @holding_registers, @input_registers)
52
-
53
- resp = tx_id + "\0\0" + (pdu.size + 1).to_word + @uid.chr + pdu
54
- log "Server TX (#{resp.size} bytes): #{logging_bytes(resp)}"
55
- io.write resp
56
- else
57
- log "Ignored server RX (invalid unit ID #{unit_id}, #{req.size} bytes): #{logging_bytes(req)}"
58
- end
50
+ next unless proto_id == "\x00\x00"
51
+
52
+ req = io.read(len - 1)
53
+ log "Server RX (#{req.size} bytes): #{logging_bytes(req)}"
54
+
55
+ func = req.getbyte(0)
56
+ params = parse_request(func, req)
57
+ pdu = exec_req(unit_id, func, params, req)
58
+
59
+ if pdu
60
+ resp = "#{tx_id}\x00\x00#{(pdu.size + 1).to_word}#{unit_id.chr}#{pdu}"
61
+ log "Server TX (#{resp.size} bytes): #{logging_bytes(resp)}"
62
+ io.write resp
63
+ else
64
+ log "Ignored server RX (invalid unit ID #{unit_id}, #{req.size} bytes): #{logging_bytes(req)}"
59
65
  end
60
66
  end
61
67
  end
62
- end
68
+ end
63
69
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModBus
2
4
  # TCP slave implementation
3
5
  # @example
@@ -7,25 +9,26 @@ module ModBus
7
9
  # end
8
10
  # end
9
11
  #
10
- # @see RTUViaTCPClient#open_connection
12
+ # @see TCP#open_tcp_connection
11
13
  # @see Client#with_slave
12
14
  # @see Slave
13
- class TCPSlave < Slave
15
+ class TCPSlave < Client::Slave
14
16
  attr_reader :transaction
15
17
 
16
18
  # @see Slave::initialize
17
19
  def initialize(uid, io)
18
20
  @transaction = 0
19
- super(uid, io)
21
+ super
20
22
  end
21
23
 
22
24
  private
25
+
23
26
  # overide method for RTU over TCP implamentaion
24
27
  # @see Slave#query
25
28
  def send_pdu(pdu)
26
- @transaction = 0 if @transaction.next > 65535
29
+ @transaction = 0 if @transaction.next > 65_535
27
30
  @transaction += 1
28
- msg = @transaction.to_word + "\0\0" + (pdu.size + 1).to_word + @uid.chr + pdu
31
+ msg = "#{@transaction.to_word}\x00\x00#{(pdu.size + 1).to_word}#{@uid.chr}#{pdu}"
29
32
  @io.write msg
30
33
 
31
34
  log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
@@ -36,19 +39,17 @@ module ModBus
36
39
  def read_pdu
37
40
  loop do
38
41
  header = @io.read(7)
39
- if header
40
- trn = header[0,2].unpack('n')[0]
41
- len = header[4,2].unpack('n')[0]
42
- msg = @io.read(len-1)
43
-
44
- log "Rx (#{(header + msg).size} bytes): " + logging_bytes(header + msg)
45
-
46
- if trn == @transaction
47
- return msg
48
- else
49
- log "Transaction number mismatch. A packet is ignored."
50
- end
51
- end
42
+ next unless header
43
+
44
+ trn = header[0, 2].unpack1("n")
45
+ len = header[4, 2].unpack1("n")
46
+ msg = @io.read(len - 1)
47
+
48
+ log "Rx (#{(header + msg).size} bytes): " + logging_bytes(header + msg)
49
+
50
+ return msg if trn == @transaction
51
+
52
+ log "Transaction number mismatch. A packet is ignored."
52
53
  end
53
54
  end
54
55
  end
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ModBus
2
- # Package version
3
- VERSION = '1.3.2'
4
+ VERSION = "2.1.2"
4
5
  end