rmodbus 0.1.1 → 0.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.
- 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
|