rmodbus 0.1.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +1 -2
- data/CHANGES +6 -0
- data/README +0 -0
- data/ext/extconf.rb +13 -0
- data/ext/serialport.c +1312 -0
- data/lib/rmodbus.rb +1 -0
- data/lib/rmodbus/client.rb +54 -6
- data/lib/rmodbus/exceptions.rb +4 -1
- data/lib/rmodbus/rtu_client.rb +6 -0
- data/lib/rmodbus/tcp_client.rb +30 -8
- data/lib/rmodbus/tcp_server.rb +158 -0
- metadata +9 -6
- data/lib/rmodbus/adu.rb +0 -36
data/lib/rmodbus.rb
CHANGED
data/lib/rmodbus/client.rb
CHANGED
@@ -54,11 +54,43 @@ class Integer
|
|
54
54
|
end
|
55
55
|
end
|
56
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
|
+
|
57
86
|
module ModBus
|
58
87
|
|
59
88
|
class Client
|
60
89
|
|
61
90
|
include Errors
|
91
|
+
# Number of times to retry on connection and read timeouts
|
92
|
+
CONNECTION_RETRIES = 10
|
93
|
+
READ_RETRIES = 10
|
62
94
|
|
63
95
|
# Read value *ncoils* coils starting with *addr*
|
64
96
|
#
|
@@ -70,10 +102,16 @@ module ModBus
|
|
70
102
|
# Read value *ncoils* discrete inputs starting with *addr*
|
71
103
|
#
|
72
104
|
# Return array of their values
|
73
|
-
def
|
105
|
+
def read_discrete_inputs(addr, ncoils)
|
74
106
|
query("\x2" + addr.to_bytes + ncoils.to_bytes).to_array_bit[0..ncoils-1]
|
75
107
|
end
|
76
108
|
|
109
|
+
# Deprecated version of read_discrete_inputs
|
110
|
+
def read_discret_inputs(addr, ncoils)
|
111
|
+
warn "[DEPRECATION] `read_discret_inputs` is deprecated. Please use `read_discrete_inputs` instead."
|
112
|
+
read_discrete_inputs(addr, ncoils)
|
113
|
+
end
|
114
|
+
|
77
115
|
# Read value *nreg* holding registers starting with *addr*
|
78
116
|
#
|
79
117
|
# Return array of their values
|
@@ -159,8 +197,15 @@ module ModBus
|
|
159
197
|
def query(pdu)
|
160
198
|
send_pdu(pdu)
|
161
199
|
|
162
|
-
|
163
|
-
|
200
|
+
tried = 0
|
201
|
+
begin
|
202
|
+
timeout(1, ModBusTimeout) do
|
203
|
+
pdu = read_pdu
|
204
|
+
end
|
205
|
+
rescue ModBusTimeout => err
|
206
|
+
tried += 1
|
207
|
+
retry unless tried >= READ_RETRIES
|
208
|
+
raise ModBusTimeout.new, 'Timed out during read attempt'
|
164
209
|
end
|
165
210
|
|
166
211
|
if pdu[0].to_i >= 0x80
|
@@ -176,23 +221,26 @@ module ModBus
|
|
176
221
|
when 5
|
177
222
|
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"
|
178
223
|
when 6
|
179
|
-
raise
|
224
|
+
raise SlaveDeviceBus.new, "The server is engaged in processing a long duration program command"
|
180
225
|
when 8
|
181
226
|
raise MemoryParityError.new, "The extended file area failed to pass a consistency check"
|
182
227
|
else
|
183
|
-
raise ModBusException.new, "
|
228
|
+
raise ModBusException.new, "Unknown error"
|
184
229
|
end
|
185
230
|
end
|
186
231
|
pdu[2..-1]
|
187
232
|
end
|
188
233
|
|
189
|
-
|
234
|
+
protected
|
190
235
|
def send_pdu(pdu)
|
191
236
|
end
|
192
237
|
|
193
238
|
def read_pdu
|
194
239
|
end
|
195
240
|
|
241
|
+
def close
|
242
|
+
end
|
243
|
+
|
196
244
|
end
|
197
245
|
|
198
246
|
end
|
data/lib/rmodbus/exceptions.rb
CHANGED
@@ -33,12 +33,15 @@ module ModBus
|
|
33
33
|
class Acknowledge < ModBusException
|
34
34
|
end
|
35
35
|
|
36
|
-
class
|
36
|
+
class SlaveDeviceBus < ModBusException
|
37
37
|
end
|
38
38
|
|
39
39
|
class MemoryParityError < ModBusException
|
40
40
|
end
|
41
41
|
|
42
|
+
class ModBusTimeout < ModBusException
|
43
|
+
end
|
44
|
+
|
42
45
|
end
|
43
46
|
|
44
47
|
end
|
data/lib/rmodbus/tcp_client.rb
CHANGED
@@ -15,7 +15,6 @@ require 'socket'
|
|
15
15
|
require 'timeout'
|
16
16
|
require 'rmodbus/client'
|
17
17
|
require 'rmodbus/exceptions'
|
18
|
-
require 'rmodbus/adu'
|
19
18
|
|
20
19
|
module ModBus
|
21
20
|
|
@@ -24,26 +23,49 @@ module ModBus
|
|
24
23
|
|
25
24
|
include Timeout
|
26
25
|
|
26
|
+
@@transaction = 0
|
27
|
+
|
27
28
|
# Connect with a ModBus server
|
28
29
|
def initialize(ipaddr, port = 502, slaveaddr = 1)
|
29
|
-
|
30
|
+
|
31
|
+
tried = 0
|
32
|
+
begin
|
33
|
+
timeout(1, ModBusTimeout) do
|
30
34
|
@sock = TCPSocket.new(ipaddr, port)
|
31
35
|
end
|
36
|
+
rescue ModBusTimeout => err
|
37
|
+
tried += 1
|
38
|
+
retry unless tried >= CONNECTION_RETRIES
|
39
|
+
raise ModBusTimeout.new, 'Timed out attempting to create connection'
|
40
|
+
end
|
32
41
|
@slave = slaveaddr
|
33
42
|
end
|
43
|
+
|
44
|
+
def close
|
45
|
+
@sock.close unless @sock.closed?
|
46
|
+
end
|
34
47
|
|
48
|
+
|
49
|
+
def self.transaction
|
50
|
+
@@transaction
|
51
|
+
end
|
52
|
+
|
35
53
|
private
|
36
54
|
def send_pdu(pdu)
|
37
|
-
|
38
|
-
@sock.write @
|
55
|
+
@@transaction += 1
|
56
|
+
@sock.write @@transaction.to_bytes + "\0\0" + (pdu.size + 1).to_bytes + @slave.chr + pdu
|
39
57
|
end
|
40
58
|
|
41
59
|
def read_pdu
|
42
60
|
header = @sock.read(7)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
61
|
+
if header
|
62
|
+
tin = header[0,2].to_int16
|
63
|
+
raise Errors::ModBusException.new("Transaction number mismatch") unless tin == @@transaction
|
64
|
+
len = header[4,2].to_int16
|
65
|
+
@sock.read(len-1)
|
66
|
+
else
|
67
|
+
raise Errors::ModBusException.new("Server did not respond")
|
68
|
+
end
|
47
69
|
end
|
48
70
|
|
49
71
|
end
|
@@ -0,0 +1,158 @@
|
|
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 'rmodbus/exceptions'
|
15
|
+
require 'gserver'
|
16
|
+
|
17
|
+
|
18
|
+
module ModBus
|
19
|
+
|
20
|
+
class TCPServer < GServer
|
21
|
+
|
22
|
+
attr_accessor :coils, :discret_inputs, :holding_registers, :input_registers
|
23
|
+
|
24
|
+
@@funcs = [1,2,3,4,5,6,15,16]
|
25
|
+
|
26
|
+
def initialize(port = 502, uid = 1)
|
27
|
+
@coils = []
|
28
|
+
@discret_inputs = []
|
29
|
+
@holding_registers =[]
|
30
|
+
@input_registers = []
|
31
|
+
@uid = uid
|
32
|
+
super(port)
|
33
|
+
end
|
34
|
+
|
35
|
+
def serve(io)
|
36
|
+
req = io.read(7)
|
37
|
+
if req[2,2] != "\x00\x00" or req[6].to_i != @uid
|
38
|
+
io.close
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
tr = req[0,2]
|
43
|
+
len = req[4,2].to_int16
|
44
|
+
req = io.read(len - 1)
|
45
|
+
func = req[0].to_i
|
46
|
+
|
47
|
+
unless @@funcs.include?(func)
|
48
|
+
param = { :err => 1 }
|
49
|
+
end
|
50
|
+
|
51
|
+
case func
|
52
|
+
when 1
|
53
|
+
param = parse_read_func(req, @coils)
|
54
|
+
if param[:err] == 0
|
55
|
+
val = @coils[param[:addr],param[:quant]].bits_to_bytes
|
56
|
+
res = func.chr + val.size.chr + val
|
57
|
+
end
|
58
|
+
when 2
|
59
|
+
param = parse_read_func(req, @discret_inputs)
|
60
|
+
if param[:err] == 0
|
61
|
+
val = @discret_inputs[param[:addr],param[:quant]].bits_to_bytes
|
62
|
+
res = func.chr + val.size.chr + val
|
63
|
+
end
|
64
|
+
when 3
|
65
|
+
param = parse_read_func(req, @holding_registers)
|
66
|
+
if param[:err] == 0
|
67
|
+
res = func.chr + (param[:quant] * 2).chr + @holding_registers[param[:addr],param[:quant]].to_ints16
|
68
|
+
end
|
69
|
+
when 4
|
70
|
+
param = parse_read_func(req, @input_registers)
|
71
|
+
if param[:err] == 0
|
72
|
+
res = func.chr + (param[:quant] * 2).chr + @input_registers[param[:addr],param[:quant]].to_ints16
|
73
|
+
end
|
74
|
+
when 5
|
75
|
+
param = parse_write_coil_func(req)
|
76
|
+
if param[:err] == 0
|
77
|
+
@coils[param[:addr]] = param[:val]
|
78
|
+
res = func.chr + req
|
79
|
+
end
|
80
|
+
when 6
|
81
|
+
param = parse_write_register_func(req)
|
82
|
+
if param[:err] == 0
|
83
|
+
@holding_registers[param[:addr]] = param[:val]
|
84
|
+
res = func.chr + req
|
85
|
+
end
|
86
|
+
when 15
|
87
|
+
param = parse_write_multiple_coils_func(req)
|
88
|
+
if param[:err] == 0
|
89
|
+
@coils[param[:addr],param[:quant]] = param[:val][0,param[:quant]]
|
90
|
+
res = func.chr + req
|
91
|
+
end
|
92
|
+
when 16
|
93
|
+
param = parse_write_multiple_registers_func(req)
|
94
|
+
if param[:err] == 0
|
95
|
+
@holding_registers[param[:addr],param[:quant]] = param[:val][0,param[:quant]]
|
96
|
+
res = func.chr + req
|
97
|
+
end
|
98
|
+
end
|
99
|
+
if param[:err] == 0
|
100
|
+
io.write tr + "\0\0" + (res.size + 1).to_bytes + @uid.chr + res
|
101
|
+
else
|
102
|
+
io.write tr + "\0\0\0\3" + @uid.chr + (func | 0x80).chr + param[:err].chr
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def parse_read_func(req, field)
|
109
|
+
quant = req[3,2].to_int16
|
110
|
+
|
111
|
+
return { :err => 3} unless quant <= 0x7d
|
112
|
+
|
113
|
+
addr = req[1,2].to_int16
|
114
|
+
return { :err => 2 } unless addr + quant <= field.size
|
115
|
+
|
116
|
+
return { :err => 0, :quant => quant, :addr => addr }
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse_write_coil_func(req)
|
120
|
+
addr = req[1,2].to_int16
|
121
|
+
return { :err => 2 } unless addr <= @coils.size
|
122
|
+
|
123
|
+
val = req[3,2].to_int16
|
124
|
+
return { :err => 3 } unless val == 0 or val == 0xff00
|
125
|
+
|
126
|
+
val = 1 if val == 0xff00
|
127
|
+
return { :err => 0, :addr => addr, :val => val }
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_write_register_func(req)
|
131
|
+
addr = req[1,2].to_int16
|
132
|
+
return { :err => 2 } unless addr <= @coils.size
|
133
|
+
|
134
|
+
val = req[3,2].to_int16
|
135
|
+
|
136
|
+
return { :err => 0, :addr => addr, :val => val }
|
137
|
+
end
|
138
|
+
|
139
|
+
def parse_write_multiple_coils_func(req)
|
140
|
+
param = parse_read_func(req, @coils)
|
141
|
+
|
142
|
+
if param[:err] == 0
|
143
|
+
param = {:err => 0, :addr => param[:addr], :quant => param[:quant], :val => req[6,param[:quant]].to_array_bit }
|
144
|
+
end
|
145
|
+
param
|
146
|
+
end
|
147
|
+
|
148
|
+
def parse_write_multiple_registers_func(req)
|
149
|
+
param = parse_read_func(req, @holding_registers)
|
150
|
+
|
151
|
+
if param[:err] == 0
|
152
|
+
param = {:err => 0, :addr => param[:addr], :quant => param[:quant], :val => req[6,param[:quant] * 2].to_array_int16 }
|
153
|
+
end
|
154
|
+
param
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rmodbus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: "0.2"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- A.Timin,
|
7
|
+
- A.Timin, J. Sanders
|
8
8
|
autorequire: rmodbus
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2009-01-30 00:00:00 +05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -25,11 +25,14 @@ extra_rdoc_files:
|
|
25
25
|
- LICENSE
|
26
26
|
- CHANGES
|
27
27
|
files:
|
28
|
+
- lib/rmodbus/rtu_client.rb
|
29
|
+
- lib/rmodbus/tcp_server.rb
|
30
|
+
- lib/rmodbus/client.rb
|
28
31
|
- lib/rmodbus/exceptions.rb
|
29
32
|
- lib/rmodbus/tcp_client.rb
|
30
|
-
- lib/rmodbus/adu.rb
|
31
|
-
- lib/rmodbus/client.rb
|
32
33
|
- lib/rmodbus.rb
|
34
|
+
- ext/extconf.rb
|
35
|
+
- ext/serialport.c
|
33
36
|
- README
|
34
37
|
- AUTHORS
|
35
38
|
- LICENSE
|
@@ -60,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
63
|
requirements: []
|
61
64
|
|
62
65
|
rubyforge_project:
|
63
|
-
rubygems_version: 1.
|
66
|
+
rubygems_version: 1.3.1
|
64
67
|
signing_key:
|
65
68
|
specification_version: 2
|
66
69
|
summary: RModBus - free implementation of protocol ModBus
|
data/lib/rmodbus/adu.rb
DELETED
@@ -1,36 +0,0 @@
|
|
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
|
-
class ADU
|
17
|
-
|
18
|
-
@@transaction_id = 0
|
19
|
-
|
20
|
-
attr_reader :unit_id, :transaction_id, :pdu, :size
|
21
|
-
|
22
|
-
def initialize(pdu, uid)
|
23
|
-
@pdu = pdu
|
24
|
-
@size = pdu.size + 1
|
25
|
-
@unit_id = uid
|
26
|
-
@transaction_id = @@transaction_id
|
27
|
-
@@transaction_id += 1
|
28
|
-
end
|
29
|
-
|
30
|
-
def serialize
|
31
|
-
@transaction_id.to_bytes + "\x00\x00" + @size.to_bytes + @unit_id.chr + pdu
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
end
|