rmodbus 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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