rmodbus 0.4.0 → 0.5.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 CHANGED
@@ -1,2 +1,3 @@
1
1
  Timin Aleksey
2
2
  James Sanders
3
+ Kelley Reynolds
data/ChangeLog CHANGED
@@ -54,3 +54,29 @@
54
54
  SerialPort::NONE, SerialPort::EVEN, SerialPort::ODD
55
55
  2010-01-20 Timin Aleksey <atimin@gmail.com>
56
56
  * lib/rmodbus/ext.rb: Fixed bug for divisible 8 data in Array#pack_to_word
57
+ 2010-02-6 Timin Aleksey <atimin@gmail.com>
58
+ * lib/rmodbus/client.rb, lib/rmodbus/tcp_client.rb: Client#connection_retries don't use more
59
+ 2010-07-16 Timin Aleksey <atimin@gmail.com>
60
+ * lib/rmodbus/rtu_client.rb: Added option read_timeout and accessor RTUClient#read_timeout
61
+ 2010-07-28 Kelley Reynolds <kelley@insidesystems.net>
62
+ * lib/rmodbus/rtu_via_tcp_client.rb: Added the ability to send RTU commands via TCP Serial Gateway
63
+ 2010-07-31 Kelley Reynolds <kelley@insidesystems.net>
64
+ * examples/use_rtu_via_tcp_modbus.rb: Added example for using RTUViaTCP client and server
65
+ * examples/use_tcp_modbus.rb: Misc cleanups
66
+ * lib/rmodbus.rb: Include the RTUViaTCPServer and Common files
67
+ * lib/rmodbus/client.rb: Move logging to Common
68
+ * lib/rmodbus/common.rb: File which houses common logging methods
69
+ * lib/rmodbus/rtu_client.rb: Logging updates
70
+ * lib/rmodbus/rtu_via_tcp_client.rb: Added RTUViaTCPClient#read_modbus_rtu_response() to abstract reading the correct number of bytes for RTU via TCP
71
+ * lib/rmodbus/rtu_via_tcp_server.rb: Added RTUViaTCPServer for use with testing RTUViaTCPClient
72
+ * lib/rmodbus/tcp_client.rb: Logging updates
73
+ * lib/rmodbus/tcp_server.rb: Logging updates
74
+ * spec/logging_spec.rb: Updated to new logging
75
+ 2011-02-02 Timin Aleksey <atimin@gmail>
76
+ * lib/rmodbus/client.rb, lib/rmodbus/rtu_client.rb : Remove RTUViaTCPClient#read_mb_rtu_response -> Client#read_rtu_response. Added support for exception and mask sunction.
77
+ * lib/rmodbus/rtu_client.rb : use #read_rtu_response for read pdu
78
+ * spec/read_rtu_response_spec.rb : add test for Client#read_rtu_response method
79
+ * spec/rtu_via_tcp_client_spec.rb : add test for RTUViaTCP class
80
+ 2011-02-10 Timin Aleksey <atimin@gmail.com>
81
+ * lib/rmodbus/client.rb : Fixed retry of request after end timeout. Added property #read_retry_timeout.
82
+ * rmodbus.gemspec : New 0.5.0 release
data/README CHANGED
@@ -5,7 +5,7 @@
5
5
  == Features
6
6
 
7
7
  * Support Ruby 1.8, Ruby 1.9
8
- * Support ModBus-TCP, ModBus-RTU protocol
8
+ * Support TCP, RTU, RTU over TCP protocols
9
9
  * Support client(master) and server(slave)
10
10
  * Support functions:
11
11
  * 01 (0x01) Read Coils
@@ -28,11 +28,13 @@ $ gem install --remote rmodbus
28
28
 
29
29
  require 'rmodbus'
30
30
 
31
- cl = ModBus::TCPClient.new('127.0.0.1', 8502, 1)
31
+ cl = ModBus::TCPClient.new('127.0.0.1', 8502, 1) do |cl|
32
32
 
33
- puts cl.read_holding_registers(0,4)
33
+ puts cl.read_holding_registers(0,4)
34
34
 
35
- cl.write_multiple_registers(0, [4,4,4])
35
+ cl.write_multiple_registers(0, [4,4,4])
36
+
37
+ end
36
38
 
37
39
  == GitHub
38
40
 
@@ -0,0 +1,19 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib/')
2
+ require 'rmodbus'
3
+
4
+ srv = ModBus::RTUViaTCPServer.new(10002,1)
5
+ srv.coils = [1,0,1,1]
6
+ srv.discrete_inputs = [1,1,0,0]
7
+ srv.holding_registers = [1,2,3,4]
8
+ srv.input_registers = [1,2,3,4]
9
+ srv.debug = true
10
+ srv.start
11
+
12
+ ModBus::RTUViaTCPClient.connect('127.0.0.1', 10002, 1) do |cl|
13
+ cl.debug = true
14
+ puts cl.read_holding_registers(0,4).inspect
15
+ cl.write_multiple_registers(0, [4,4,4])
16
+ puts cl.read_holding_registers(0,4).inspect
17
+ end
18
+
19
+ srv.shutdown
@@ -10,9 +10,10 @@ srv.debug = true
10
10
  srv.audit = true
11
11
  srv.start
12
12
 
13
- cl = ModBus::TCPClient.new('127.0.0.1', 8502, 1)
14
- cl.debug = true
15
- puts cl.read_holding_registers(0,4)
16
- cl.write_multiple_registers(0, [4,4,4])
17
- puts cl.read_holding_registers(0,4)
13
+ ModBus::TCPClient.connect('127.0.0.1', 8502, 1) do |cl|
14
+ cl.debug = true
15
+ puts cl.read_holding_registers(0,4)
16
+ cl.write_multiple_registers(0, [4,4,4])
17
+ puts cl.read_holding_registers(0,4)
18
+ end
18
19
  srv.stop
@@ -9,8 +9,11 @@
9
9
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
  # GNU General Public License for more details.
12
+ require 'rmodbus/common'
12
13
  require 'rmodbus/tcp_client'
13
14
  require 'rmodbus/tcp_server'
14
15
  require 'rmodbus/rtu_client'
15
16
  require 'rmodbus/rtu_server'
17
+ require 'rmodbus/rtu_via_tcp_client'
18
+ require 'rmodbus/rtu_via_tcp_server'
16
19
 
@@ -12,21 +12,42 @@
12
12
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
13
  # GNU General Public License for more details.
14
14
 
15
+ require 'rmodbus/common'
15
16
  require 'rmodbus/exceptions'
16
17
  require 'rmodbus/ext'
17
18
 
18
19
 
19
20
  module ModBus
20
-
21
21
  class Client
22
-
23
22
  include Errors
23
+ include Common
24
24
  # Number of times to retry on connection and read timeouts
25
- attr_accessor :connection_retries, :read_retries
25
+ attr_accessor :read_retries, :read_retry_timeout
26
+
27
+ def connection_retries
28
+ warn "[DEPRECATION] `connection_retries` is deprecated. Please don't use it."
29
+ @connection_retries
30
+ end
26
31
 
32
+ def connection_retries=(value)
33
+ warn "[DEPRECATION] `connection_retries=` is deprecated. Please don't use it."
34
+ @connection_retries = value
35
+ end
36
+
37
+
38
+ Exceptions = {
39
+ 1 => IllegalFunction.new("The function code received in the query is not an allowable action for the server"),
40
+ 2 => IllegalDataAddress.new("The data address received in the query is not an allowable address for the server"),
41
+ 3 => IllegalDataValue.new("A value contained in the query data field is not an allowable value for server"),
42
+ 4 => SlaveDeviceFailure.new("An unrecoverable error occurred while the server was attempting to perform the requested action"),
43
+ 5 => Acknowledge.new("The server has accepted the request and is processing it, but a long duration of time will be required to do so"),
44
+ 6 => SlaveDeviceBus.new("The server is engaged in processing a long duration program command"),
45
+ 8 => MemoryParityError.new("The extended file area failed to pass a consistency check")
46
+ }
27
47
  def initialize
28
48
  @connection_retries = 10
29
49
  @read_retries = 10
50
+ @read_retry_timeout = 1
30
51
  end
31
52
  # Read value *ncoils* coils starting with *addr*
32
53
  #
@@ -44,7 +65,7 @@ module ModBus
44
65
 
45
66
  # Deprecated version of read_discrete_inputs
46
67
  def read_discret_inputs(addr, ncoils)
47
- warn "[DEPRECATION] `read_discret_inputs` is deprecated. Please use `read_discrete_inputs` instead."
68
+ #warn "[DEPRECATION] `read_discret_inputs` is deprecated. Please use `read_discrete_inputs` instead."
48
69
  read_discrete_inputs(addr, ncoils)
49
70
  end
50
71
 
@@ -131,12 +152,14 @@ module ModBus
131
152
  end
132
153
 
133
154
  def query(pdu)
134
- send_pdu(pdu)
135
-
136
155
  tried = 0
137
156
  begin
138
- timeout(1, ModBusTimeout) { pdu = read_pdu }
157
+ timeout(@read_retry_timeout, ModBusTimeout) do
158
+ send_pdu(pdu)
159
+ pdu = read_pdu
160
+ end
139
161
  rescue ModBusTimeout => err
162
+ log "Timeout of read operation: (#{@read_retries - tried})"
140
163
  tried += 1
141
164
  retry unless tried >= @read_retries
142
165
  raise ModBusTimeout.new, "Timed out during read attempt"
@@ -145,29 +168,16 @@ module ModBus
145
168
  return nil if pdu.size == 0
146
169
 
147
170
  if pdu.getbyte(0) >= 0x80
148
- case pdu.getbyte(1)
149
- when 1
150
- raise IllegalFunction.new, "The function code received in the query is not an allowable action for the server"
151
- when 2
152
- raise IllegalDataAddress.new, "The data address received in the query is not an allowable address for the server"
153
- when 3
154
- raise IllegalDataValue.new, "A value contained in the query data field is not an allowable value for server"
155
- when 4
156
- raise SlaveDeviceFailure.new, "An unrecoverable error occurred while the server was attempting to perform the requested action"
157
- when 5
158
- 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"
159
- when 6
160
- raise SlaveDeviceBus.new, "The server is engaged in processing a long duration program command"
161
- when 8
162
- raise MemoryParityError.new, "The extended file area failed to pass a consistency check"
163
- else
164
- raise ModBusException.new, "Unknown error"
165
- end
171
+ exc_id = pdu.getbyte(1)
172
+ raise Exceptions[exc_id] unless Exceptions[exc_id].nil?
173
+
174
+ raise ModBusException.new, "Unknown error"
166
175
  end
167
176
  pdu[2..-1]
168
177
  end
169
178
 
170
179
  protected
180
+
171
181
  def send_pdu(pdu)
172
182
  end
173
183
 
@@ -177,20 +187,27 @@ module ModBus
177
187
  def close
178
188
  end
179
189
 
180
- private
181
- def logging_bytes(msg)
182
- result = ""
183
- msg.each_byte do |c|
184
- byte = if c < 16
185
- '0' + c.to_s(16)
190
+ # We have to read specific amounts of numbers of bytes from the network depending on the function code and content
191
+ def read_rtu_response(io)
192
+ # Read the slave_id and function code
193
+ msg = io.read(2)
194
+ function_code = msg.getbyte(1)
195
+ case function_code
196
+ when 1,2,3,4 then
197
+ # read the third byte to find out how much more
198
+ # we need to read + CRC
199
+ msg += io.read(1)
200
+ msg += io.read(msg.getbyte(2)+2)
201
+ when 5,6,15,16 then
202
+ # We just read in an additional 6 bytes
203
+ msg += io.read(6)
204
+ when 22 then
205
+ msg += io.read(8)
206
+ when 0x80..0xff then
207
+ msg += io.read(4)
186
208
  else
187
- c.to_s(16)
188
- end
189
- result << "[#{byte}]"
209
+ raise ModBus::Errors::IllegalFunction, "Illegal function: #{function_code}"
190
210
  end
191
- result
192
211
  end
193
-
194
212
  end
195
-
196
213
  end
@@ -0,0 +1,23 @@
1
+ module ModBus
2
+ module Common
3
+
4
+ private
5
+ def log(msg)
6
+ $stdout.puts msg if @debug
7
+ end
8
+
9
+ def logging_bytes(msg)
10
+ result = ""
11
+ msg.each_byte do |c|
12
+ byte = if c < 16
13
+ '0' + c.to_s(16)
14
+ else
15
+ c.to_s(16)
16
+ end
17
+ result << "[#{byte}]"
18
+ end
19
+ result
20
+ end
21
+ end
22
+ end
23
+
@@ -22,8 +22,8 @@ class String
22
22
 
23
23
  def unpack_bits
24
24
  array_bit = []
25
- self.unpack('b*')[0].each_byte do |b|
26
- array_bit << b.chr.to_i
25
+ self.unpack('b*')[0].each_char do |c|
26
+ array_bit << c.to_i
27
27
  end
28
28
  array_bit
29
29
  end
@@ -24,7 +24,7 @@ module ModBus
24
24
  class RTUClient < Client
25
25
 
26
26
  include CRC16
27
- attr_reader :port, :baud, :slave, :data_bits, :stop_bits, :parity
27
+ attr_reader :port, :baud, :slave, :data_bits, :stop_bits, :parity, :read_timeout
28
28
  attr_accessor :debug
29
29
 
30
30
  # Connect with RTU server
@@ -81,19 +81,22 @@ module ModBus
81
81
  # :stop_bits => 1 or 2
82
82
  #
83
83
  # :parity => NONE, EVEN or ODD
84
+ #
85
+ # :read_timeout => default 5 ms
84
86
  def initialize(port, baud=9600, slaveaddr=1, options = {})
85
87
  @port, @baud, @slave = port, baud, slaveaddr
86
88
 
87
- @data_bits, @stop_bits, @parity = 8, 1, SerialPort::NONE
89
+ @data_bits, @stop_bits, @parity, @read_timeout = 8, 1, SerialPort::NONE, 5
88
90
 
89
91
  @data_bits = options[:data_bits] unless options[:data_bits].nil?
90
92
  @stop_bits = options[:stop_bits] unless options[:stop_bits].nil?
91
93
  @parity = options[:parity] unless options[:parity].nil?
94
+ @read_timeout = options[:read_timeout] unless options[:read_timeout].nil?
92
95
 
93
96
  @debug = false
94
97
 
95
98
  @sp = SerialPort.new(@port, @baud, @data_bits, @stop_bits, @parity)
96
- @sp.read_timeout = 5
99
+ @sp.read_timeout = @read_timeout
97
100
 
98
101
  super()
99
102
  end
@@ -111,23 +114,20 @@ module ModBus
111
114
  msg = @slave.chr + pdu
112
115
  msg << crc16(msg).to_word
113
116
  @sp.write msg
114
- if @debug
115
- STDOUT << "Tx (#{msg.size} bytes): " + logging_bytes(msg) + "\n"
116
- end
117
+
118
+ log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
117
119
  end
118
120
 
119
121
  def read_pdu
120
- msg = ''
121
- while msg.size == 0
122
- msg = @sp.read
123
- end
122
+ msg = read_rtu_response(@sp)
124
123
 
125
- if @debug
126
- STDOUT << "Rx (#{msg.size} bytes): " + logging_bytes(msg) + "\n"
127
- end
124
+ log "Rx (#{msg.size} bytes): " + logging_bytes(msg)
128
125
 
129
126
  if msg.getbyte(0) == @slave
130
127
  return msg[1..-3] if msg[-2,2].unpack('n')[0] == crc16(msg[0..-3])
128
+ log "Ignore package: don't match CRC"
129
+ else
130
+ log "Ignore package: don't match slave ID"
131
131
  end
132
132
  loop do
133
133
  #waite timeout
@@ -0,0 +1,105 @@
1
+ # RModBus - free implementation of ModBus protocol in Ruby.
2
+ #
3
+ # Copyright (C) 2009 Timin Aleksey
4
+ # Copyright (C) 2010 Kelley Reynolds
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ require 'rmodbus/crc16'
16
+ require 'timeout'
17
+ require 'rmodbus/client'
18
+ require 'rmodbus/exceptions'
19
+
20
+ module ModBus
21
+
22
+ class RTUViaTCPClient < Client
23
+
24
+ include CRC16
25
+ attr_reader :ipaddr, :port, :slave
26
+ attr_accessor :debug
27
+
28
+ # Connect with Serial TCP Gateway (eg barionet-50)
29
+ #
30
+ # ipaddr - ip of the server
31
+ #
32
+ # port - port TCP connections
33
+ #
34
+ # slaveaddr - slave ID of the server
35
+ #
36
+ # RTUViaTCPClient.connect('127.0.0.1') do |cl|
37
+ #
38
+ # put cl.read_holding_registers(0, 10)
39
+ #
40
+ # end
41
+ def self.connect(ipaddr, port = 10002, slaveaddr = 1)
42
+ cl = RTUViaTCPClient.new(ipaddr, port, slaveaddr)
43
+ yield cl
44
+ cl.close
45
+ end
46
+
47
+ # Connect with a ModBus server
48
+ #
49
+ # ipaddr - ip of the server
50
+ #
51
+ # port - port TCP connections
52
+ #
53
+ # slaveaddr - slave ID of the server
54
+ def initialize(ipaddr, port = 10002, slaveaddr = 1)
55
+ @ipaddr, @port = ipaddr, port
56
+ tried = 0
57
+ begin
58
+ timeout(1, ModBusTimeout) do
59
+ @sock = TCPSocket.new(@ipaddr, @port)
60
+ end
61
+ rescue ModBusTimeout => err
62
+ raise ModBusTimeout.new, 'Timed out attempting to create connection'
63
+ end
64
+ @slave = slaveaddr
65
+ @debug = false
66
+ super()
67
+ end
68
+
69
+ # Close TCP connections
70
+ def close
71
+ @sock.close unless @sock.closed?
72
+ end
73
+
74
+ # Check TCP connections
75
+ def closed?
76
+ @sock.closed?
77
+ end
78
+
79
+ protected
80
+
81
+ def send_pdu(pdu)
82
+ msg = @slave.chr + pdu
83
+ msg << crc16(msg).to_word
84
+ @sock.write msg
85
+
86
+ log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
87
+ end
88
+
89
+ def read_pdu
90
+ # Read the response appropriately
91
+ msg = read_rtu_response(@sock)
92
+
93
+ log "Rx (#{msg.size} bytes): " + logging_bytes(msg)
94
+ if msg.getbyte(0) == @slave
95
+ return msg[1..-3] if msg[-2,2].unpack('n')[0] == crc16(msg[0..-3])
96
+ log "Ignore package: don't match CRC"
97
+ else
98
+ log "Ignore package: don't match slave ID"
99
+ end
100
+ loop do
101
+ #waite timeout
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,114 @@
1
+ # RModBus - free implementation of ModBus protocol in Ruby.
2
+ #
3
+ # Copyright (C) 2010 Timin Aleksey
4
+ # Copyright (C) 2010 Kelley Reynolds
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+
16
+ require 'rmodbus/parsers'
17
+ require 'gserver'
18
+
19
+ module ModBus
20
+
21
+ class RTUViaTCPServer < GServer
22
+ include Parsers
23
+ include CRC16
24
+ include Common
25
+
26
+ attr_accessor :coils, :discrete_inputs, :holding_registers, :input_registers, :debug
27
+
28
+ def discret_inputs
29
+ warn "[DEPRECATION] `discret_inputs` is deprecated. Please use `discrete_inputs` instead."
30
+ @discrete_inputs
31
+ end
32
+
33
+ def discret_inputs=(val)
34
+ warn "[DEPRECATION] `discret_inputs=` is deprecated. Please use `discrete_inputs=` instead."
35
+ @discrete_inputs=val
36
+ end
37
+
38
+
39
+ def initialize(port = 10002, slave = 1)
40
+ @coils = []
41
+ @discrete_inputs = []
42
+ @holding_registers =[]
43
+ @input_registers = []
44
+ @slave = slave
45
+ super(port)
46
+ end
47
+
48
+ def serve(io)
49
+ loop do
50
+ # read the RTU message
51
+ msg = read_modbus_rtu_request(io)
52
+
53
+ # If there is no RTU message, we're done serving this client
54
+ break if msg.nil?
55
+
56
+ if msg.getbyte(0) == @slave and msg[-2,2].unpack('n')[0] == crc16(msg[0..-3])
57
+ pdu = exec_req(msg[1..-3], @coils, @discrete_inputs, @holding_registers, @input_registers)
58
+ resp = @slave.chr + pdu
59
+ resp << crc16(resp).to_word
60
+ log "Server TX (#{resp.size} bytes): #{logging_bytes(resp)}"
61
+ io.write resp
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ # We have to read specific amounts of numbers of bytes from the network depending on the function code and content
69
+ # NOTE: The initial read could be increased to 7 and that would let us cobine the two reads for functions 15 and 16 but this method is more clear
70
+ def read_modbus_rtu_request(io)
71
+ # Read the slave_id and function code
72
+ msg = io.read(2)
73
+
74
+ # If msg is nil, then our client never sent us anything and it's time to disconnect
75
+ return if msg.nil?
76
+
77
+ function_code = msg.getbyte(1)
78
+ if [1, 2, 3, 4, 5, 6].include?(function_code)
79
+ # read 6 more bytes and return the message total message
80
+ msg += io.read(6)
81
+ elsif [15, 16].include?(function_code)
82
+ # Read in first register, register count, and data bytes
83
+ msg += io.read(5)
84
+ # Read in however much data we need to + 2 CRC bytes
85
+ msg += io.read(msg.getbyte(6) + 2)
86
+ else
87
+ raise ModBus::Errors::IllegalFunction, "Illegal function: #{function_code}"
88
+ end
89
+
90
+ log "Server RX (#{msg.size} bytes): #{logging_bytes(msg)}"
91
+
92
+ msg
93
+ end
94
+
95
+ def log(msg)
96
+ if @debug
97
+ $stdout.puts msg
98
+ end
99
+ end
100
+
101
+ def logging_bytes(msg)
102
+ result = ""
103
+ msg.each_byte do |c|
104
+ byte = if c < 16
105
+ '0' + c.to_s(16)
106
+ else
107
+ c.to_s(16)
108
+ end
109
+ result << "[#{byte}]"
110
+ end
111
+ result
112
+ end
113
+ end
114
+ end
@@ -23,10 +23,8 @@ module ModBus
23
23
 
24
24
  include Timeout
25
25
 
26
- attr_reader :ipaddr, :port, :slave
26
+ attr_reader :ipaddr, :port, :slave, :transaction
27
27
  attr_accessor :debug
28
-
29
- @@transaction = 0
30
28
 
31
29
  # Connect with ModBus server
32
30
  #
@@ -55,15 +53,14 @@ module ModBus
55
53
  #
56
54
  # slaveaddr - slave ID of the server
57
55
  def initialize(ipaddr, port = 502, slaveaddr = 1)
56
+ @transaction = 0
58
57
  @ipaddr, @port = ipaddr, port
59
58
  tried = 0
60
59
  begin
61
60
  timeout(1, ModBusTimeout) do
62
- @sock = TCPSocket.new(@ipaddr, @port)
63
- end
61
+ @sock = TCPSocket.new(@ipaddr, @port)
62
+ end
64
63
  rescue ModBusTimeout => err
65
- tried += 1
66
- retry unless tried >= @connection_retries
67
64
  raise ModBusTimeout.new, 'Timed out attempting to create connection'
68
65
  end
69
66
  @slave = slaveaddr
@@ -81,32 +78,25 @@ module ModBus
81
78
  @sock.closed?
82
79
  end
83
80
 
84
- def self.transaction
85
- @@transaction
86
- end
87
-
88
81
  private
89
82
  def send_pdu(pdu)
90
- @@transaction = 0 if @@transaction.next > 65535
91
- @@transaction += 1
92
- msg = @@transaction.to_word + "\0\0" + (pdu.size + 1).to_word + @slave.chr + pdu
83
+ @transaction = 0 if @transaction.next > 65535
84
+ @transaction += 1
85
+ msg = @transaction.to_word + "\0\0" + (pdu.size + 1).to_word + @slave.chr + pdu
93
86
  @sock.write msg
94
87
 
95
- if debug
96
- STDOUT << "Tx (#{msg.size} bytes): " + logging_bytes(msg) + "\n"
97
- end
88
+ log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
98
89
  end
99
90
 
100
91
  def read_pdu
101
92
  header = @sock.read(7)
102
93
  if header
103
94
  tin = header[0,2].unpack('n')[0]
104
- raise Errors::ModBusException.new("Transaction number mismatch") unless tin == @@transaction
95
+ raise Errors::ModBusException.new("Transaction number mismatch") unless tin == @transaction
105
96
  len = header[4,2].unpack('n')[0]
106
97
  msg = @sock.read(len-1)
107
- if @debug
108
- STDOUT << "Rx (#{(header + msg).size} bytes): " + logging_bytes(header + msg) + "\n"
109
- end
98
+
99
+ log "Rx (#{(header + msg).size} bytes): " + logging_bytes(header + msg)
110
100
  msg
111
101
  else
112
102
  raise Errors::ModBusException.new("Server did not respond")
@@ -14,49 +14,51 @@
14
14
  require 'rmodbus/parsers'
15
15
  require 'gserver'
16
16
 
17
-
18
17
  module ModBus
19
- class TCPServer < GServer
20
- include Parsers
21
-
22
- attr_accessor :coils, :discrete_inputs, :holding_registers, :input_registers
23
-
24
- def discret_inputs
25
- warn "[DEPRECATION] `discret_inputs` is deprecated. Please use `discrete_inputs` instead."
26
- @discrete_inputs
27
- end
28
-
29
- def discret_inputs=(val)
30
- warn "[DEPRECATION] `discret_inputs=` is deprecated. Please use `discrete_inputs=` instead."
31
- @discrete_inputs=val
32
- end
33
-
34
-
35
- def initialize(port = 502, uid = 1)
36
- @coils = []
37
- @discrete_inputs = []
38
- @holding_registers =[]
39
- @input_registers = []
40
- @uid = uid
41
- super(port)
42
- end
43
-
44
- def serve(io)
45
- loop do
46
- req = io.read(7)
47
- if req[2,2] != "\x00\x00" or req.getbyte(6) != @uid
48
- io.close
49
- break
50
- end
51
-
52
- tr = req[0,2]
53
- len = req[4,2].unpack('n')[0]
54
- req = io.read(len - 1)
55
-
56
- pdu = exec_req(req, @coils, @discrete_inputs, @holding_registers, @input_registers)
57
-
58
- io.write tr + "\0\0" + (pdu.size + 1).to_word + @uid.chr + pdu
59
- end
60
- end
61
- end
18
+ class TCPServer < GServer
19
+ include Parsers
20
+ include Common
21
+
22
+ attr_accessor :coils, :discrete_inputs, :holding_registers, :input_registers, :debug
23
+
24
+ def discret_inputs
25
+ warn "[DEPRECATION] `discret_inputs` is deprecated. Please use `discrete_inputs` instead."
26
+ @discrete_inputs
27
+ end
28
+
29
+ def discret_inputs=(val)
30
+ warn "[DEPRECATION] `discret_inputs=` is deprecated. Please use `discrete_inputs=` instead."
31
+ @discrete_inputs=val
32
+ end
33
+
34
+ def initialize(port = 502, uid = 1)
35
+ @coils = []
36
+ @discrete_inputs = []
37
+ @holding_registers =[]
38
+ @input_registers = []
39
+ @uid = uid
40
+ super(port)
41
+ end
42
+
43
+ def serve(io)
44
+ loop do
45
+ req = io.read(7)
46
+ if req[2,2] != "\x00\x00" or req.getbyte(6) != @uid
47
+ io.close
48
+ break
49
+ end
50
+
51
+ tr = req[0,2]
52
+ len = req[4,2].unpack('n')[0]
53
+ req = io.read(len - 1)
54
+ log "Server RX (#{req.size} bytes): #{logging_bytes(req)}"
55
+
56
+ pdu = exec_req(req, @coils, @discrete_inputs, @holding_registers, @input_registers)
57
+
58
+ resp = tr + "\0\0" + (pdu.size + 1).to_word + @uid.chr + pdu
59
+ log "Server TX (#{resp.size} bytes): #{logging_bytes(resp)}"
60
+ io.write resp
61
+ end
62
+ end
63
+ end
62
64
  end
@@ -4,6 +4,7 @@ describe Array do
4
4
 
5
5
  before do
6
6
  @arr = [1,0,1,1, 0,0,1,1, 1,1,0,1, 0,1,1,0, 1,0,1]
7
+ @test = [0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0]
7
8
  end
8
9
 
9
10
  it "should return string reprisent 16bit" do
@@ -13,6 +14,10 @@ describe Array do
13
14
  it "fixed bug for divisible 8 data " do
14
15
  ([0] * 8).pack_to_word.should == "\x00"
15
16
  end
17
+
18
+ it "should unpack to @test" do
19
+ "test".unpack_bits == @test
20
+ end
16
21
 
17
22
  end
18
23
 
@@ -20,8 +20,8 @@ describe TCPClient do
20
20
  request, response = "\x3\x0\x6b\x0\x3", "\x3\x6\x2\x2b\x0\x0\x0\x64"
21
21
  mock_query(request,response)
22
22
  @mb_client.debug = true
23
- STDOUT.should_receive("<<").with("Tx (12 bytes): [00][01][00][00][00][06][01][03][00][6b][00][03]\n")
24
- STDOUT.should_receive("<<").with("Rx (15 bytes): [00][01][00][00][00][09][01][03][06][02][2b][00][00][00][64]\n")
23
+ $stdout.should_receive(:puts).with("Tx (12 bytes): [00][01][00][00][00][06][01][03][00][6b][00][03]")
24
+ $stdout.should_receive(:puts).with("Rx (15 bytes): [00][01][00][00][00][09][01][03][06][02][2b][00][00][00][64]")
25
25
  @mb_client.query(request)
26
26
  end
27
27
 
@@ -33,7 +33,7 @@ describe TCPClient do
33
33
 
34
34
 
35
35
  def mock_query(request, response)
36
- @adu = TCPClient.transaction.next.to_word + "\x0\x0\x0\x9" + UID.chr + request
36
+ @adu = @mb_client.transaction.next.to_word + "\x0\x0\x0\x9" + UID.chr + request
37
37
  @sock.should_receive(:write).with(@adu[0,4] + "\0\6" + UID.chr + request)
38
38
  @sock.should_receive(:read).with(7).and_return(@adu[0,7])
39
39
  @sock.should_receive(:read).with(8).and_return(response)
@@ -56,11 +56,13 @@ describe RTUClient do
56
56
  it 'should log rec\send bytes' do
57
57
  request = "\x3\x0\x1\x0\x1"
58
58
  @sp.should_receive(:write).with("\1#{request}\xd5\xca")
59
- @sp.should_receive(:read).and_return("\x1\x3\x2\xff\xff\xb9\xf4")
59
+ @sp.should_receive(:read).with(2).and_return("\x1\x3")
60
+ @sp.should_receive(:read).with(1).and_return("\x2")
61
+ @sp.should_receive(:read).with(4).and_return("\xff\xff\xb9\xf4")
60
62
 
61
63
  @mb_client.debug = true
62
- STDOUT.should_receive("<<").with("Tx (8 bytes): [01][03][00][01][00][01][d5][ca]\n")
63
- STDOUT.should_receive("<<").with("Rx (7 bytes): [01][03][02][ff][ff][b9][f4]\n")
64
+ $stdout.should_receive(:puts).with("Tx (8 bytes): [01][03][00][01][00][01][d5][ca]")
65
+ $stdout.should_receive(:puts).with("Rx (7 bytes): [01][03][02][ff][ff][b9][f4]")
64
66
 
65
67
  @mb_client.query(request).should == "\xff\xff"
66
68
  end
@@ -0,0 +1,93 @@
1
+ require 'rmodbus/client'
2
+
3
+ include ModBus
4
+
5
+ #Use public wrap method
6
+
7
+ class Client
8
+ def test_read_method(msg)
9
+ io = TestIO.new(msg)
10
+ read_rtu_response(io)
11
+ end
12
+
13
+ end
14
+
15
+ class TestIO
16
+ def initialize(msg)
17
+ @msg = msg
18
+ end
19
+
20
+ def read(num)
21
+ result = @msg[0,num]
22
+ @msg = @msg[num..-1]
23
+ result
24
+ end
25
+ end
26
+
27
+ describe "#read_rtu_response" do
28
+ before do
29
+ @cl_mb = Client.new
30
+ end
31
+
32
+ it "should read response for 'read coils'" do
33
+ resp = make_resp("\x1\x3\xcd\x6b\x05")
34
+ @cl_mb.test_read_method(resp).should == resp
35
+ end
36
+
37
+ it "should read response for 'read discrete inputs'" do
38
+ resp = make_resp("\x2\x3\xac\xdb\x35")
39
+ @cl_mb.test_read_method(resp).should == resp
40
+ end
41
+
42
+ it "should read response for 'read holding registers'" do
43
+ resp = make_resp("\x3\x6\x2\x2b\x0\x0\x0\x64")
44
+ @cl_mb.test_read_method(resp).should == resp
45
+ end
46
+
47
+ it "should read response for 'read input registers'" do
48
+ resp = make_resp("\x4\x2\x0\xa")
49
+ @cl_mb.test_read_method(resp).should == resp
50
+ end
51
+
52
+ it "should read response for 'write single coil'" do
53
+ resp = make_resp("\x5\x0\xac\xff\x0")
54
+ @cl_mb.test_read_method(resp).should == resp
55
+ end
56
+
57
+ it "should read response for 'write single register'" do
58
+ resp = make_resp("\x6\x0\x1\x0\x3")
59
+ @cl_mb.test_read_method(resp).should == resp
60
+ end
61
+
62
+ it "should read response for 'write multiple coils'" do
63
+ resp = make_resp("\xf\x0\x13\x0\xa")
64
+ @cl_mb.test_read_method(resp).should == resp
65
+ end
66
+
67
+ it "should read response for 'write multiple registers'" do
68
+ resp = make_resp("\x10\x0\x1\x0\x2")
69
+ @cl_mb.test_read_method(resp).should == resp
70
+ end
71
+
72
+ it "should read response 'mask write register'" do
73
+ resp = make_resp("\x16\x0\x4\x0\xf2\x0\x25")
74
+ @cl_mb.test_read_method(resp).should == resp
75
+ end
76
+
77
+ it "should read exception codes" do
78
+ resp = make_resp("\x84\x3")
79
+ @cl_mb.test_read_method(resp).should == resp
80
+ end
81
+
82
+ it "should raise exception if function is illegal" do
83
+ resp = make_resp("\xff\x0\x1\x0\x2").should raise_error {
84
+ ModBus::Errors::IllegalFunction
85
+ }
86
+ end
87
+
88
+
89
+ def make_resp(msg)
90
+ "\x1" + msg + "\x2\x2" # slave + msg + mock_crc
91
+ end
92
+ end
93
+
@@ -12,6 +12,7 @@ describe RTUClient do
12
12
  @sp = mock('Serial port')
13
13
  SerialPort.should_receive(:new).with("/dev/port1", 9600, 8, 1, 0).and_return(@sp)
14
14
  @sp.stub!(:read_timeout=)
15
+ @sp.stub!(:read)
15
16
 
16
17
  @mb_client = RTUClient.new("/dev/port1", 9600, 1,
17
18
  :data_bits => 8, :stop_bits => 1, :parity => SerialPort::NONE)
@@ -21,21 +22,25 @@ describe RTUClient do
21
22
  it "should ignore frame with other UID" do
22
23
  request = "\x10\x0\x1\x0\x1\x2\xff\xff"
23
24
  @sp.should_receive(:write).with("\1#{request}\xA6\x31")
24
- @sp.should_receive(:read).and_return("\x2\x10\x0\x1\x0\x1\x1C\x08")
25
+ @sp.should_receive(:read).with(2).and_return("\x2\x10")
26
+ @sp.should_receive(:read).with(6).and_return("\x0\x1\x0\x1\x1C\x08")
25
27
  lambda {@mb_client.query(request)}.should raise_error(ModBus::Errors::ModBusTimeout)
26
28
  end
27
29
 
28
30
  it "should ignored frame with incorrect CRC" do
29
31
  request = "\x10\x0\x1\x0\x1\x2\xff\xff"
30
32
  @sp.should_receive(:write).with("\1#{request}\xA6\x31")
31
- @sp.should_receive(:read).and_return("\x1\x10\x0\x1\x0\x1\x1C\x08")
33
+ @sp.should_receive(:read).with(2).and_return("\x2\x10")
34
+ @sp.should_receive(:read).with(6).and_return("\x0\x1\x0\x1\x1C\x08")
32
35
  lambda {@mb_client.query(request)}.should raise_error(ModBus::Errors::ModBusTimeout)
33
36
  end
34
37
 
35
38
  it "should return value of registers"do
36
39
  request = "\x3\x0\x1\x0\x1"
37
40
  @sp.should_receive(:write).with("\1#{request}\xd5\xca")
38
- @sp.should_receive(:read).and_return("\x1\x3\x2\xff\xff\xb9\xf4")
41
+ @sp.should_receive(:read).with(2).and_return("\x1\x3")
42
+ @sp.should_receive(:read).with(1).and_return("\x2")
43
+ @sp.should_receive(:read).with(4).and_return("\xff\xff\xb9\xf4")
39
44
  @mb_client.query(request).should == "\xff\xff"
40
45
  end
41
46
 
@@ -0,0 +1,72 @@
1
+ begin
2
+ require 'rubygems'
3
+ rescue
4
+ end
5
+ require 'rmodbus'
6
+
7
+ include ModBus
8
+
9
+ describe RTUViaTCPClient do
10
+
11
+ before do
12
+ @sock = mock('Socked')
13
+ TCPSocket.should_receive(:new).with("127.0.0.1", 10002).and_return(@sock)
14
+ @sock.stub!(:read_timeout=)
15
+ @sock.stub!(:read)
16
+
17
+ @mb_client = RTUViaTCPClient.new("127.0.0.1")
18
+ @mb_client.read_retries = 0
19
+ end
20
+
21
+ it "should ignore frame with other UID" do
22
+ request = "\x10\x0\x1\x0\x1\x2\xff\xff"
23
+ @sock.should_receive(:write).with("\1#{request}\xA6\x31")
24
+ @sock.should_receive(:read).with(2).and_return("\x2\x10")
25
+ @sock.should_receive(:read).with(6).and_return("\x0\x1\x0\x1\x1C\x08")
26
+ lambda {@mb_client.query(request)}.should raise_error(ModBus::Errors::ModBusTimeout)
27
+ end
28
+
29
+ it "should ignored frame with incorrect CRC" do
30
+ request = "\x10\x0\x1\x0\x1\x2\xff\xff"
31
+ @sock.should_receive(:write).with("\1#{request}\xA6\x31")
32
+ @sock.should_receive(:read).with(2).and_return("\x2\x10")
33
+ @sock.should_receive(:read).with(6).and_return("\x0\x1\x0\x1\x1C\x08")
34
+ lambda {@mb_client.query(request)}.should raise_error(ModBus::Errors::ModBusTimeout)
35
+ end
36
+
37
+ it "should return value of registers"do
38
+ request = "\x3\x0\x1\x0\x1"
39
+ @sock.should_receive(:write).with("\1#{request}\xd5\xca")
40
+ @sock.should_receive(:read).with(2).and_return("\x1\x3")
41
+ @sock.should_receive(:read).with(1).and_return("\x2")
42
+ @sock.should_receive(:read).with(4).and_return("\xff\xff\xb9\xf4")
43
+ @mb_client.query(request).should == "\xff\xff"
44
+ end
45
+
46
+ it 'should sugar connect method' do
47
+ ipaddr, port, slave = '127.0.0.1', 502, 3
48
+ TCPSocket.should_receive(:new).with(ipaddr, port).and_return(@sock)
49
+ @sock.should_receive(:closed?).and_return(false)
50
+ @sock.should_receive(:close)
51
+ RTUViaTCPClient.connect(ipaddr, port, slave) do |cl|
52
+ cl.ipaddr.should == ipaddr
53
+ cl.port.should == port
54
+ cl.slave.should == slave
55
+ end
56
+ end
57
+
58
+ it 'should have closed? method' do
59
+ @sock.should_receive(:closed?).and_return(false)
60
+ @mb_client.closed?.should == false
61
+
62
+ @sock.should_receive(:closed?).and_return(false)
63
+ @sock.should_receive(:close)
64
+
65
+ @mb_client.close
66
+
67
+ @sock.should_receive(:closed?).and_return(true)
68
+ @mb_client.closed?.should == true
69
+ end
70
+
71
+ end
72
+
@@ -17,14 +17,14 @@ describe TCPClient, "method 'query'" do
17
17
  end
18
18
 
19
19
  it 'should send valid MBAP Header' do
20
- @adu[0,2] = TCPClient.transaction.next.to_word
20
+ @adu[0,2] = @mb_client.transaction.next.to_word
21
21
  @sock.should_receive(:write).with(@adu)
22
22
  @sock.should_receive(:read).with(7).and_return(@adu)
23
23
  @mb_client.query('').should == nil
24
24
  end
25
25
 
26
26
  it 'should throw exception if get other transaction' do
27
- @adu[0,2] = TCPClient.transaction.next.to_word
27
+ @adu[0,2] = @mb_client.transaction.next.to_word
28
28
  @sock.should_receive(:write).with(@adu)
29
29
  @sock.should_receive(:read).with(7).and_return("\000\002\000\000\000\001" + UID.chr)
30
30
  begin
@@ -37,7 +37,7 @@ describe TCPClient, "method 'query'" do
37
37
  it 'should return only data from PDU' do
38
38
  request = "\x3\x0\x6b\x0\x3"
39
39
  response = "\x3\x6\x2\x2b\x0\x0\x0\x64"
40
- @adu = TCPClient.transaction.next.to_word + "\x0\x0\x0\x9" + UID.chr + request
40
+ @adu = @mb_client.transaction.next.to_word + "\x0\x0\x0\x9" + UID.chr + request
41
41
  @sock.should_receive(:write).with(@adu[0,4] + "\0\6" + UID.chr + request)
42
42
  @sock.should_receive(:read).with(7).and_return(@adu[0,7])
43
43
  @sock.should_receive(:read).with(8).and_return(response)
metadata CHANGED
@@ -1,27 +1,39 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rmodbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ hash: 11
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 5
9
+ - 0
10
+ version: 0.5.0
5
11
  platform: ruby
6
12
  authors:
7
- - A.Timin, J. Sanders
13
+ - A.Timin, J. Sanders, K. Reynolds
8
14
  autorequire: rmodbus
9
15
  bindir: bin
10
16
  cert_chain: []
11
17
 
12
- date: 2010-01-23 00:00:00 +05:00
18
+ date: 2011-02-10 00:00:00 +05:00
13
19
  default_executable:
14
20
  dependencies:
15
21
  - !ruby/object:Gem::Dependency
16
22
  name: serialport
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
20
26
  requirements:
21
27
  - - ">="
22
28
  - !ruby/object:Gem::Version
23
- version: 1.0.1
24
- version:
29
+ hash: 31
30
+ segments:
31
+ - 1
32
+ - 0
33
+ - 4
34
+ version: 1.0.4
35
+ type: :runtime
36
+ version_requirements: *id001
25
37
  description:
26
38
  email: atimin@gmail.com
27
39
  executables: []
@@ -34,28 +46,34 @@ extra_rdoc_files:
34
46
  - LICENSE
35
47
  - ChangeLog
36
48
  files:
37
- - lib/rmodbus/tcp_client.rb
49
+ - lib/rmodbus.rb
50
+ - lib/rmodbus/client.rb
51
+ - lib/rmodbus/crc16.rb
38
52
  - lib/rmodbus/parsers.rb
39
53
  - lib/rmodbus/rtu_client.rb
54
+ - lib/rmodbus/ext.rb
55
+ - lib/rmodbus/rtu_via_tcp_client.rb
40
56
  - lib/rmodbus/tcp_server.rb
41
- - lib/rmodbus/rtu_server.rb
42
57
  - lib/rmodbus/exceptions.rb
43
- - lib/rmodbus/client.rb
44
- - lib/rmodbus/ext.rb
45
- - lib/rmodbus/crc16.rb
46
- - lib/rmodbus.rb
58
+ - lib/rmodbus/rtu_server.rb
59
+ - lib/rmodbus/rtu_via_tcp_server.rb
60
+ - lib/rmodbus/common.rb
61
+ - lib/rmodbus/tcp_client.rb
62
+ - examples/add_new_function.rb
47
63
  - examples/use_tcp_modbus.rb
48
- - examples/perfomance_rtu.rb
49
64
  - examples/perfomance_tcp.rb
50
- - examples/add_new_function.rb
51
- - spec/logging_spec.rb
65
+ - examples/use_rtu_via_tcp_modbus.rb
66
+ - examples/perfomance_rtu.rb
52
67
  - spec/ext_spec.rb
53
- - spec/tcp_client_spec.rb
54
- - spec/rtu_client_spec.rb
55
68
  - spec/exception_spec.rb
56
- - spec/client_spec.rb
57
69
  - spec/tcp_server_spec.rb
70
+ - spec/logging_spec.rb
71
+ - spec/client_spec.rb
58
72
  - spec/rtu_server_spec.rb
73
+ - spec/tcp_client_spec.rb
74
+ - spec/rtu_via_tcp_client_spec.rb
75
+ - spec/read_rtu_response_spec.rb
76
+ - spec/rtu_client_spec.rb
59
77
  - Rakefile
60
78
  - README
61
79
  - AUTHORS
@@ -75,21 +93,27 @@ rdoc_options:
75
93
  require_paths:
76
94
  - lib
77
95
  required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
78
97
  requirements:
79
98
  - - ">="
80
99
  - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
81
103
  version: "0"
82
- version:
83
104
  required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
84
106
  requirements:
85
107
  - - ">="
86
108
  - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
87
112
  version: "0"
88
- version:
89
113
  requirements: []
90
114
 
91
115
  rubyforge_project: RModBus
92
- rubygems_version: 1.3.5
116
+ rubygems_version: 1.3.7
93
117
  signing_key:
94
118
  specification_version: 3
95
119
  summary: RModBus - free implementation of protocol ModBus