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