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/lib/rmodbus.rb CHANGED
@@ -10,3 +10,4 @@
10
10
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
  # GNU General Public License for more details.
12
12
  require 'rmodbus/tcp_client'
13
+ require 'rmodbus/tcp_server'
@@ -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 read_discret_inputs(addr, ncoils)
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
- timeout(1) do
163
- pdu = read_pdu
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 SlaveDiviceBus.new, "The server is engaged in processing a long duration program command"
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, "Unknow error"
228
+ raise ModBusException.new, "Unknown error"
184
229
  end
185
230
  end
186
231
  pdu[2..-1]
187
232
  end
188
233
 
189
- protected
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
@@ -33,12 +33,15 @@ module ModBus
33
33
  class Acknowledge < ModBusException
34
34
  end
35
35
 
36
- class SlaveDiviceBus < ModBusException
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
@@ -0,0 +1,6 @@
1
+ module ModBus
2
+
3
+ class RTUClient
4
+ end
5
+
6
+ end
@@ -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
- timeout(1) do
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
- @adu = ADU.new(pdu,@slave)
38
- @sock.write @adu.serialize
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
- tin = header[0,2].to_int16
44
- raise Errors::ModBusException.new("Transaction number mismatch") unless tin == @adu.transaction_id
45
- len = header[4,2].to_int16
46
- @sock.read(len-1)
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.1.1
4
+ version: "0.2"
5
5
  platform: ruby
6
6
  authors:
7
- - A.Timin, D.Samatov
7
+ - A.Timin, J. Sanders
8
8
  autorequire: rmodbus
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-01-25 00:00:00 +05:00
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.0.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