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.
- checksums.yaml +7 -0
- data/NEWS.md +180 -0
- data/README.md +115 -0
- data/Rakefile +29 -0
- data/examples/perfomance_rtu.rb +56 -0
- data/examples/perfomance_rtu_via_tcp.rb +55 -0
- data/examples/perfomance_tcp.rb +55 -0
- data/examples/simple-xpca-gateway.rb +84 -0
- data/examples/use_rtu_via_tcp_modbus.rb +22 -0
- data/examples/use_tcp_modbus.rb +23 -0
- data/lib/rmodbus.rb +21 -0
- data/lib/rmodbus/client.rb +94 -0
- data/lib/rmodbus/client/slave.rb +345 -0
- data/lib/rmodbus/debug.rb +25 -0
- data/lib/rmodbus/errors.rb +42 -0
- data/lib/rmodbus/ext.rb +85 -0
- data/lib/rmodbus/options.rb +6 -0
- data/lib/rmodbus/proxy.rb +41 -0
- data/lib/rmodbus/rtu.rb +122 -0
- data/lib/rmodbus/rtu_client.rb +43 -0
- data/lib/rmodbus/rtu_server.rb +48 -0
- data/lib/rmodbus/rtu_slave.rb +48 -0
- data/lib/rmodbus/rtu_via_tcp_server.rb +35 -0
- data/lib/rmodbus/server.rb +246 -0
- data/lib/rmodbus/server/slave.rb +16 -0
- data/lib/rmodbus/sp.rb +36 -0
- data/lib/rmodbus/tcp.rb +31 -0
- data/lib/rmodbus/tcp_client.rb +25 -0
- data/lib/rmodbus/tcp_server.rb +67 -0
- data/lib/rmodbus/tcp_slave.rb +55 -0
- data/lib/rmodbus/version.rb +3 -0
- data/spec/client_spec.rb +88 -0
- data/spec/exception_spec.rb +120 -0
- data/spec/ext_spec.rb +52 -0
- data/spec/logging_spec.rb +89 -0
- data/spec/proxy_spec.rb +74 -0
- data/spec/read_rtu_response_spec.rb +92 -0
- data/spec/response_mismach_spec.rb +163 -0
- data/spec/rtu_client_spec.rb +86 -0
- data/spec/rtu_server_spec.rb +31 -0
- data/spec/rtu_via_tcp_client_spec.rb +76 -0
- data/spec/rtu_via_tcp_server_spec.rb +89 -0
- data/spec/slave_spec.rb +55 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/tcp_client_spec.rb +88 -0
- data/spec/tcp_server_spec.rb +158 -0
- 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
|