rmodbus-ccutrer 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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