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.
- checksums.yaml +5 -5
- data/NEWS.md +19 -0
- data/README.md +8 -8
- data/examples/perfomance_rtu.rb +55 -56
- data/examples/perfomance_rtu_via_tcp.rb +54 -55
- data/examples/perfomance_tcp.rb +54 -55
- data/examples/simple_xpca_gateway.rb +85 -0
- data/examples/use_rtu_via_tcp_modbus.rb +14 -11
- data/examples/use_tcp_modbus.rb +14 -11
- data/lib/rmodbus/client/slave.rb +333 -0
- data/lib/rmodbus/client.rb +15 -10
- data/lib/rmodbus/debug.rb +12 -15
- data/lib/rmodbus/errors.rb +26 -2
- data/lib/rmodbus/ext.rb +72 -51
- data/lib/rmodbus/options.rb +4 -1
- data/lib/rmodbus/proxy.rb +14 -9
- data/lib/rmodbus/rtu.rb +89 -125
- data/lib/rmodbus/rtu_client.rb +22 -2
- data/lib/rmodbus/rtu_server.rb +16 -12
- data/lib/rmodbus/rtu_slave.rb +26 -3
- data/lib/rmodbus/rtu_via_tcp_server.rb +12 -19
- data/lib/rmodbus/server/slave.rb +18 -0
- data/lib/rmodbus/server.rb +227 -84
- data/lib/rmodbus/sp.rb +10 -12
- data/lib/rmodbus/tcp.rb +9 -10
- data/lib/rmodbus/tcp_client.rb +3 -0
- data/lib/rmodbus/tcp_server.rb +41 -35
- data/lib/rmodbus/tcp_slave.rb +19 -18
- data/lib/rmodbus/version.rb +3 -2
- data/lib/rmodbus.rb +20 -21
- metadata +63 -50
- data/Rakefile +0 -29
- data/examples/simple-xpca-gateway.rb +0 -84
- data/lib/rmodbus/rtu_via_tcp_client.rb +0 -26
- data/lib/rmodbus/rtu_via_tcp_slave.rb +0 -29
- data/lib/rmodbus/slave.rb +0 -308
- data/spec/client_spec.rb +0 -88
- data/spec/exception_spec.rb +0 -119
- data/spec/ext_spec.rb +0 -52
- data/spec/logging_spec.rb +0 -89
- data/spec/proxy_spec.rb +0 -74
- data/spec/read_rtu_response_spec.rb +0 -92
- data/spec/response_mismach_spec.rb +0 -163
- data/spec/rtu_client_spec.rb +0 -86
- data/spec/rtu_server_spec.rb +0 -30
- data/spec/rtu_via_tcp_client_spec.rb +0 -76
- data/spec/rtu_via_tcp_server_spec.rb +0 -16
- data/spec/slave_spec.rb +0 -55
- data/spec/spec_helper.rb +0 -54
- data/spec/tcp_client_spec.rb +0 -88
- data/spec/tcp_server_spec.rb +0 -129
data/lib/rmodbus/server.rb
CHANGED
@@ -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
|
-
|
6
|
+
autoload :Slave, "rmodbus/server/slave"
|
7
|
+
|
8
|
+
attr_accessor :promiscuous, :request_callback, :response_callback
|
5
9
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
15
|
-
|
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
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
71
|
-
|
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
|
177
|
+
pdu
|
74
178
|
end
|
75
179
|
end
|
76
180
|
|
77
|
-
def parse_read_func(req,
|
78
|
-
|
181
|
+
def parse_read_func(req, expected_length = 5)
|
182
|
+
return nil if expected_length && req.length != expected_length
|
79
183
|
|
80
|
-
|
184
|
+
{ quant: req[3, 2].unpack1("n"), addr: req[1, 2].unpack1("n") }
|
185
|
+
end
|
81
186
|
|
82
|
-
|
83
|
-
return
|
187
|
+
def validate_read_func(params, field, quant_max = 0x7d)
|
188
|
+
return 3 unless params[:quant] <= quant_max
|
84
189
|
|
85
|
-
|
190
|
+
2 unless params[:addr] + params[:quant] <= field.size
|
86
191
|
end
|
87
192
|
|
88
193
|
def parse_write_coil_func(req)
|
89
|
-
|
90
|
-
return { :err => 2 } unless addr <= @coils.size
|
194
|
+
return nil unless req.length == 5
|
91
195
|
|
92
|
-
|
93
|
-
|
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
|
-
|
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
|
-
|
101
|
-
return { :err => 2 } unless addr <= @holding_registers.size
|
206
|
+
return nil unless req.length == 5
|
102
207
|
|
103
|
-
|
208
|
+
{ addr: req[1, 2].unpack1("n"), val: req[3, 2].unpack1("n") }
|
209
|
+
end
|
104
210
|
|
105
|
-
|
106
|
-
|
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
|
-
|
216
|
+
return nil if req.length < 7
|
110
217
|
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
230
|
+
return nil if req.length < 8
|
119
231
|
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
3
|
-
rescue
|
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"
|
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
|
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
|
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
|
-
|
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
|
-
|
2
|
-
|
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 [
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
data/lib/rmodbus/tcp_client.rb
CHANGED
@@ -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)
|
data/lib/rmodbus/tcp_server.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
begin
|
2
|
-
require
|
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
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
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].
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
68
|
+
end
|
63
69
|
end
|
data/lib/rmodbus/tcp_slave.rb
CHANGED
@@ -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
|
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
|
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 >
|
29
|
+
@transaction = 0 if @transaction.next > 65_535
|
27
30
|
@transaction += 1
|
28
|
-
msg = @transaction.to_word
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|