sanderjd-rmodbus 0.1.3 → 0.2.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.
- data/AUTHORS +3 -0
- data/CHANGES +12 -0
- data/LICENSE +675 -0
- data/README +49 -0
- data/Rakefile +49 -0
- data/VERSION.yml +4 -0
- data/lib/rmodbus.rb +13 -0
- data/lib/rmodbus/client.rb +247 -0
- data/lib/rmodbus/exceptions.rb +47 -0
- data/lib/rmodbus/tcp_client.rb +72 -0
- data/lib/rmodbus/tcp_server.rb +160 -0
- data/spec/client_spec.rb +57 -0
- data/spec/ext_spec.rb +27 -0
- data/spec/rtu_client_spec.rb +13 -0
- data/spec/tcp_client_spec.rb +48 -0
- data/spec/tcp_server_spec.rb +107 -0
- metadata +20 -4
- data/ext/extconf.rb +0 -13
data/README
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
= RModBus
|
2
|
+
|
3
|
+
*RModBus* - free implementation of protocol ModBus.
|
4
|
+
|
5
|
+
== Feature
|
6
|
+
|
7
|
+
* Client ModBus-TCP
|
8
|
+
* Support functions:
|
9
|
+
* 01 (0x01) Read Coils
|
10
|
+
* 02 (0x02) Read Discrete Inputs
|
11
|
+
* 03 (0x03) Read Holding Registers
|
12
|
+
* 04 (0x04) Read Input Registers
|
13
|
+
* 05 (0x05) Write Single Coil
|
14
|
+
* 06 (0x06) Write Single Register
|
15
|
+
* 15 (0x0F) Write Multiple Coils
|
16
|
+
* 16 (0x10) Write Multiple registers
|
17
|
+
|
18
|
+
== Installation
|
19
|
+
|
20
|
+
You can install RModBus with the following command
|
21
|
+
|
22
|
+
$ rake install
|
23
|
+
|
24
|
+
from its distribution directory
|
25
|
+
|
26
|
+
== Gem installation
|
27
|
+
|
28
|
+
Download and install RModBus with the following
|
29
|
+
|
30
|
+
$ gem sources -a http://gems.github.com
|
31
|
+
$ gem install -r sanderjd-rmodbus
|
32
|
+
|
33
|
+
== CSM
|
34
|
+
|
35
|
+
You can checkout source code from the GIT repositry
|
36
|
+
|
37
|
+
$ git clone git://github.com/sanderjd/rmodbus.git
|
38
|
+
|
39
|
+
== Reference
|
40
|
+
|
41
|
+
RModBus project: http://rubyforge.org/projects/rmodbus
|
42
|
+
|
43
|
+
ModBus community: http://www.modbus-ida.org
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue Exception
|
4
|
+
end
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'spec/rake/spectask'
|
8
|
+
|
9
|
+
Spec::Rake::SpecTask.new do |t|
|
10
|
+
t.spec_opts = ['-c']
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
13
|
+
t.rcov = true
|
14
|
+
end
|
15
|
+
rescue Exception
|
16
|
+
puts 'RSpec not available. Install it with: sudo gem install rspec'
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
require 'jeweler'
|
21
|
+
Jeweler::Tasks.new do |s|
|
22
|
+
s.name = "rmodbus"
|
23
|
+
s.summary = "RModBus - free implementation of protocol ModBus"
|
24
|
+
s.description = "A free Ruby implementation of the ModBus protocol"
|
25
|
+
s.email = 'sanderjd@gmail.com'
|
26
|
+
s.homepage = 'http://rubyforge.org/var/svn/rmodbus/trunk'
|
27
|
+
s.authors = ['Aleksey Timin', 'D.Samatov', 'James Sanders']
|
28
|
+
s.has_rdoc = true
|
29
|
+
s.files = ["VERSION.yml",
|
30
|
+
"AUTHORS",
|
31
|
+
"CHANGES",
|
32
|
+
"LICENSE",
|
33
|
+
"README",
|
34
|
+
"Rakefile",
|
35
|
+
"lib/rmodbus",
|
36
|
+
"lib/rmodbus/client.rb",
|
37
|
+
"lib/rmodbus/exceptions.rb",
|
38
|
+
"lib/rmodbus/tcp_client.rb",
|
39
|
+
"lib/rmodbus/tcp_server.rb",
|
40
|
+
"lib/rmodbus.rb",
|
41
|
+
"spec/client_spec.rb",
|
42
|
+
"spec/ext_spec.rb",
|
43
|
+
"spec/rtu_client_spec.rb",
|
44
|
+
"spec/tcp_client_spec.rb",
|
45
|
+
"spec/tcp_server_spec.rb"]
|
46
|
+
end
|
47
|
+
rescue LoadError
|
48
|
+
puts 'Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com'
|
49
|
+
end
|
data/VERSION.yml
ADDED
data/lib/rmodbus.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# RModBus - free implementation of ModBus protocol on Ruby.
|
2
|
+
# Copyright (C) 2008 Timin Aleksey
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
require 'rmodbus/tcp_client'
|
13
|
+
require 'rmodbus/tcp_server'
|
@@ -0,0 +1,247 @@
|
|
1
|
+
# RModBus - free implementation of ModBus protocol on Ruby.
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Timin Aleksey
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
|
15
|
+
require 'rmodbus/exceptions'
|
16
|
+
|
17
|
+
class String
|
18
|
+
|
19
|
+
def to_int16
|
20
|
+
self[0]*256 + self[1]
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_array_int16
|
24
|
+
array_int16 = []
|
25
|
+
i = 0
|
26
|
+
while(i < self.size) do
|
27
|
+
array_int16 << self[i]*256 + self[i+1]
|
28
|
+
i += 2
|
29
|
+
end
|
30
|
+
array_int16
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_array_bit
|
34
|
+
array_bit = []
|
35
|
+
self.each_byte do |byte|
|
36
|
+
mask = 0x01
|
37
|
+
8.times {
|
38
|
+
unless (mask & byte) == 0
|
39
|
+
array_bit << 1
|
40
|
+
else
|
41
|
+
array_bit << 0
|
42
|
+
end
|
43
|
+
mask = mask << 1
|
44
|
+
}
|
45
|
+
end
|
46
|
+
array_bit
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
class Integer
|
52
|
+
def to_bytes
|
53
|
+
(self >> 8).chr + (self & 0xff).chr
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Array
|
58
|
+
|
59
|
+
def to_ints16
|
60
|
+
s = ""
|
61
|
+
self.each do |int16|
|
62
|
+
s << int16.to_bytes
|
63
|
+
end
|
64
|
+
s
|
65
|
+
end
|
66
|
+
|
67
|
+
def bits_to_bytes
|
68
|
+
int16 = 0
|
69
|
+
s = ""
|
70
|
+
mask = 0x01
|
71
|
+
|
72
|
+
self.each do |bit|
|
73
|
+
int16 |= mask if bit > 0
|
74
|
+
mask <<= 1
|
75
|
+
if mask == 0x100
|
76
|
+
mask = 0x01
|
77
|
+
s << int16.chr
|
78
|
+
int16 = 0
|
79
|
+
end
|
80
|
+
end
|
81
|
+
s << int16.chr unless mask == 0x01
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
module ModBus
|
87
|
+
|
88
|
+
class Client
|
89
|
+
|
90
|
+
include Errors
|
91
|
+
|
92
|
+
# Number of times to retry on connection and read timeouts
|
93
|
+
CONNECTION_RETRIES = 10
|
94
|
+
READ_RETRIES = 10
|
95
|
+
|
96
|
+
# Read value *ncoils* coils starting with *addr*
|
97
|
+
#
|
98
|
+
# Return array of their values
|
99
|
+
def read_coils(addr, ncoils)
|
100
|
+
query("\x1" + addr.to_bytes + ncoils.to_bytes).to_array_bit[0..ncoils-1]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Read value *ncoils* discrete inputs starting with *addr*
|
104
|
+
#
|
105
|
+
# Return array of their values
|
106
|
+
def read_discrete_inputs(addr, ncoils)
|
107
|
+
query("\x2" + addr.to_bytes + ncoils.to_bytes).to_array_bit[0..ncoils-1]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Deprecated version of read_discrete_inputs
|
111
|
+
def read_discret_inputs(addr, ncoils)
|
112
|
+
warn "[DEPRECATION] `read_discret_inputs` is deprecated. Please use `read_discrete_inputs` instead."
|
113
|
+
read_discrete_inputs(addr, ncoils)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Read value *nreg* holding registers starting with *addr*
|
117
|
+
#
|
118
|
+
# Return array of their values
|
119
|
+
def read_holding_registers(addr, nreg)
|
120
|
+
query("\x3" + addr.to_bytes + nreg.to_bytes).to_array_int16
|
121
|
+
end
|
122
|
+
|
123
|
+
# Read value *nreg* input registers starting with *addr*
|
124
|
+
#
|
125
|
+
# Return array of their values
|
126
|
+
def read_input_registers(addr, nreg)
|
127
|
+
query("\x4" + addr.to_bytes + nreg.to_bytes).to_array_int16
|
128
|
+
end
|
129
|
+
|
130
|
+
# Write *val* in *addr* coil
|
131
|
+
#
|
132
|
+
# if *val* lager 0 write 1
|
133
|
+
#
|
134
|
+
# Return self
|
135
|
+
def write_single_coil(addr, val)
|
136
|
+
if val == 0
|
137
|
+
query("\x5" + addr.to_bytes + 0.to_bytes)
|
138
|
+
else
|
139
|
+
query("\x5" + addr.to_bytes + 0xff00.to_bytes)
|
140
|
+
end
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
# Write *val* in *addr* register
|
145
|
+
#
|
146
|
+
# Return self
|
147
|
+
def write_single_register(addr, val)
|
148
|
+
query("\x6" + addr.to_bytes + val.to_bytes)
|
149
|
+
self
|
150
|
+
end
|
151
|
+
|
152
|
+
# Write *val* in coils starting with *addr*
|
153
|
+
#
|
154
|
+
# *val* it is array of bits
|
155
|
+
#
|
156
|
+
# Return self
|
157
|
+
def write_multiple_coils(addr, val)
|
158
|
+
nbyte = ((val.size-1) >> 3) + 1
|
159
|
+
sum = 0
|
160
|
+
(val.size - 1).downto(0) do |i|
|
161
|
+
sum = sum << 1
|
162
|
+
sum |= 1 if val[i] > 0
|
163
|
+
end
|
164
|
+
|
165
|
+
s_val = ""
|
166
|
+
nbyte.times do
|
167
|
+
s_val << (sum & 0xff).chr
|
168
|
+
sum >>= 8
|
169
|
+
end
|
170
|
+
|
171
|
+
query("\xf" + addr.to_bytes + val.size.to_bytes + nbyte.chr + s_val)
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
175
|
+
# Write *val* in registers starting with *addr*
|
176
|
+
#
|
177
|
+
# *val* it is array of integer
|
178
|
+
#
|
179
|
+
# Return self
|
180
|
+
def write_multiple_registers(addr, val)
|
181
|
+
s_val = ""
|
182
|
+
val.each do |reg|
|
183
|
+
s_val << reg.to_bytes
|
184
|
+
end
|
185
|
+
|
186
|
+
query("\x10" + addr.to_bytes + val.size.to_bytes + (val.size * 2).chr + s_val)
|
187
|
+
self
|
188
|
+
end
|
189
|
+
|
190
|
+
# Write *current value & and_mask | or mask in *addr* register
|
191
|
+
#
|
192
|
+
# Return self
|
193
|
+
def mask_write_register(addr, and_mask, or_mask)
|
194
|
+
query("\x16" + addr.to_bytes + and_mask.to_bytes + or_mask.to_bytes)
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
def query(pdu)
|
199
|
+
send_pdu(pdu)
|
200
|
+
|
201
|
+
tried = 0
|
202
|
+
begin
|
203
|
+
timeout(1, ModBusTimeout) do
|
204
|
+
pdu = read_pdu
|
205
|
+
end
|
206
|
+
rescue ModBusTimeout => err
|
207
|
+
tried += 1
|
208
|
+
retry unless tried >= READ_RETRIES
|
209
|
+
raise ModBusTimeout.new, 'Timed out during read attempt'
|
210
|
+
end
|
211
|
+
|
212
|
+
if pdu[0].to_i >= 0x80
|
213
|
+
case pdu[1].to_i
|
214
|
+
when 1
|
215
|
+
raise IllegalFunction.new, "The function code received in the query is not an allowable action for the server"
|
216
|
+
when 2
|
217
|
+
raise IllegalDataAddress.new, "The data address received in the query is not an allowable address for the server"
|
218
|
+
when 3
|
219
|
+
raise IllegalDataValue.new, "A value contained in the query data field is not an allowable value for server"
|
220
|
+
when 4
|
221
|
+
raise SlaveDeviceFailure.new, "An unrecoverable error occurred while the server was attempting to perform the requested action"
|
222
|
+
when 5
|
223
|
+
raise Acknowledge.new, "The server has accepted the request and is processing it, but a long duration of time will be required to do so"
|
224
|
+
when 6
|
225
|
+
raise SlaveDeviceBus.new, "The server is engaged in processing a long duration program command"
|
226
|
+
when 8
|
227
|
+
raise MemoryParityError.new, "The extended file area failed to pass a consistency check"
|
228
|
+
else
|
229
|
+
raise ModBusException.new, "Unknown error"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
pdu[2..-1]
|
233
|
+
end
|
234
|
+
|
235
|
+
protected
|
236
|
+
def send_pdu(pdu)
|
237
|
+
end
|
238
|
+
|
239
|
+
def read_pdu
|
240
|
+
end
|
241
|
+
|
242
|
+
def close
|
243
|
+
end
|
244
|
+
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# RModBus - free implementation of ModBus protocol on Ruby.
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Timin Aleksey
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
module ModBus
|
15
|
+
|
16
|
+
module Errors
|
17
|
+
|
18
|
+
class ModBusException < Exception
|
19
|
+
end
|
20
|
+
|
21
|
+
class ModBusTimeout < ModBusException
|
22
|
+
end
|
23
|
+
|
24
|
+
class IllegalFunction < ModBusException
|
25
|
+
end
|
26
|
+
|
27
|
+
class IllegalDataAddress < ModBusException
|
28
|
+
end
|
29
|
+
|
30
|
+
class IllegalDataValue < ModBusException
|
31
|
+
end
|
32
|
+
|
33
|
+
class SlaveDeviceFailure < ModBusException
|
34
|
+
end
|
35
|
+
|
36
|
+
class Acknowledge < ModBusException
|
37
|
+
end
|
38
|
+
|
39
|
+
class SlaveDeviceBus < ModBusException
|
40
|
+
end
|
41
|
+
|
42
|
+
class MemoryParityError < ModBusException
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# RModBus - free implementation of ModBus protocol on Ruby.
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Timin Aleksey
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
require 'socket'
|
15
|
+
require 'timeout'
|
16
|
+
require 'rmodbus/client'
|
17
|
+
require 'rmodbus/exceptions'
|
18
|
+
|
19
|
+
module ModBus
|
20
|
+
|
21
|
+
# Implementation clients(master) ModBusTCP
|
22
|
+
class TCPClient < Client
|
23
|
+
|
24
|
+
include Timeout
|
25
|
+
|
26
|
+
@@transaction = 0
|
27
|
+
|
28
|
+
# Connect with a ModBus server
|
29
|
+
def initialize(ipaddr, port = 502, slaveaddr = 1)
|
30
|
+
tried = 0
|
31
|
+
begin
|
32
|
+
timeout(1, ModBusTimeout) do
|
33
|
+
@sock = TCPSocket.new(ipaddr, port)
|
34
|
+
end
|
35
|
+
rescue ModBusTimeout => err
|
36
|
+
tried += 1
|
37
|
+
retry unless tried >= CONNECTION_RETRIES
|
38
|
+
raise ModBusTimeout.new, 'Timed out attempting to create connection'
|
39
|
+
end
|
40
|
+
|
41
|
+
@slave = slaveaddr
|
42
|
+
end
|
43
|
+
|
44
|
+
def close
|
45
|
+
@sock.close unless @sock.closed?
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.transaction
|
49
|
+
@@transaction
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def send_pdu(pdu)
|
54
|
+
@@transaction += 1
|
55
|
+
@sock.write @@transaction.to_bytes + "\0\0" + (pdu.size + 1).to_bytes + @slave.chr + pdu
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_pdu
|
59
|
+
header = @sock.read(7)
|
60
|
+
if header
|
61
|
+
tin = header[0,2].to_int16
|
62
|
+
raise Errors::ModBusException.new("Transaction number mismatch") unless tin == @@transaction
|
63
|
+
len = header[4,2].to_int16
|
64
|
+
@sock.read(len-1)
|
65
|
+
else
|
66
|
+
raise Errors::ModBusException.new("Server did not respond")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|