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