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/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
+
@@ -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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 2
3
+ :patch: 0
4
+ :major: 0
@@ -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