rmodbus 0.5.0 → 1.0.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.
Files changed (46) hide show
  1. data/NEWS.md +52 -0
  2. data/README.md +87 -0
  3. data/Rakefile +22 -36
  4. data/examples/perfomance_rtu.rb +35 -37
  5. data/examples/perfomance_tcp.rb +36 -38
  6. data/examples/use_rtu_via_tcp_modbus.rb +8 -5
  7. data/examples/use_tcp_modbus.rb +10 -6
  8. data/lib/rmodbus/client.rb +52 -174
  9. data/lib/rmodbus/common.rb +45 -18
  10. data/lib/rmodbus/{exceptions.rb → errors.rb} +3 -0
  11. data/lib/rmodbus/ext.rb +25 -2
  12. data/lib/rmodbus/proxy.rb +54 -0
  13. data/lib/rmodbus/{crc16.rb → rtu.rb} +73 -2
  14. data/lib/rmodbus/rtu_client.rb +20 -116
  15. data/lib/rmodbus/rtu_server.rb +28 -57
  16. data/lib/rmodbus/rtu_slave.rb +59 -0
  17. data/lib/rmodbus/rtu_via_tcp_client.rb +22 -86
  18. data/lib/rmodbus/rtu_via_tcp_server.rb +31 -95
  19. data/lib/rmodbus/rtu_via_tcp_slave.rb +58 -0
  20. data/lib/rmodbus/{parsers.rb → server.rb} +24 -15
  21. data/lib/rmodbus/slave.rb +268 -0
  22. data/lib/rmodbus/sp.rb +45 -0
  23. data/lib/rmodbus/tcp.rb +49 -0
  24. data/lib/rmodbus/tcp_client.rb +19 -88
  25. data/lib/rmodbus/tcp_server.rb +16 -19
  26. data/lib/rmodbus/tcp_slave.rb +64 -0
  27. data/lib/rmodbus/version.rb +17 -0
  28. data/lib/rmodbus.rb +20 -4
  29. data/spec/client_spec.rb +19 -45
  30. data/spec/exception_spec.rb +26 -27
  31. data/spec/ext_spec.rb +24 -1
  32. data/spec/logging_spec.rb +31 -37
  33. data/spec/proxy_spec.rb +73 -0
  34. data/spec/read_rtu_response_spec.rb +2 -4
  35. data/spec/rtu_client_spec.rb +17 -19
  36. data/spec/rtu_server_spec.rb +1 -3
  37. data/spec/rtu_via_tcp_client_spec.rb +69 -63
  38. data/spec/slave_spec.rb +55 -0
  39. data/spec/tcp_client_spec.rb +77 -69
  40. data/spec/tcp_server_spec.rb +34 -49
  41. metadata +123 -37
  42. data/AUTHORS +0 -3
  43. data/ChangeLog +0 -82
  44. data/LICENSE +0 -675
  45. data/README +0 -53
  46. data/examples/add_new_function.rb +0 -19
@@ -1,6 +1,6 @@
1
1
  # RModBus - free implementation of ModBus protocol in Ruby.
2
2
  #
3
- # Copyright (C) 2010 Timin Aleksey
3
+ # Copyright (C) 2010-2011 Timin Aleksey
4
4
  #
5
5
  # This program is free software: you can redistribute it and/or modify
6
6
  # it under the terms of the GNU General Public License as published by
@@ -11,78 +11,49 @@
11
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
12
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
13
  # GNU General Public License for more details.
14
-
15
- require 'rmodbus/parsers'
16
-
17
- begin
18
- require 'rubygems'
19
- rescue
20
- end
21
-
22
14
  require 'serialport'
23
15
 
24
16
  module ModBus
25
-
17
+ # RTU server implementation
18
+ # @example
19
+ # srv = RTUServer.new('/dev/ttyS1', 9600, 1)
20
+ # srv.coils = [1,0,1,1]
21
+ # srv.discrete_inputs = [1,1,0,0]
22
+ # srv.holding_registers = [1,2,3,4]
23
+ # srv.input_registers = [1,2,3,4]
24
+ # srv.debug = true
25
+ # srv.start
26
26
  class RTUServer
27
- include Parsers
28
- include CRC16
29
-
30
- attr_accessor :coils, :discrete_inputs, :holding_registers, :input_registers
31
- attr_reader :port, :baud, :slave, :data_bits, :stop_bits, :parity
32
-
33
- def discret_inputs
34
- warn "[DEPRECATION] `discret_inputs` is deprecated. Please use `discrete_inputs` instead."
35
- @discrete_inputs
36
- end
37
-
38
- def discret_inputs=(val)
39
- warn "[DEPRECATION] `discret_inputs=` is deprecated. Please use `discrete_inputs=` instead."
40
- @discrete_inputs=val
41
- end
42
-
43
-
44
- def initialize(port, baud=9600, slaveaddr=1, options = {})
45
- Thread.abort_on_exception = true
46
-
47
- @port, @baud = port, baud
48
- @data_bits, @stop_bits, @parity = 8, 1, SerialPort::NONE
49
-
50
- @data_bits = options[:data_bits] unless options[:data_bits].nil?
51
- @stop_bits = options[:stop_bits] unless options[:stop_bits].nil?
52
- @parity = options[:parity] unless options[:parity].nil?
53
-
54
- @sp = SerialPort.new(@port, @baud, @data_bits, @stop_bits, @parity)
55
- @sp.read_timeout = 5
56
-
57
- @coils = []
58
- @discrete_inputs = []
59
- @holding_registers = []
60
- @input_registers = []
61
- @slave = slaveaddr
27
+ include Common
28
+ include Server
29
+ include RTU
30
+ include SP
31
+
32
+ # Init RTU server
33
+ # @param [Integer] uid slave device
34
+ # @see SP#open_serial_port
35
+ def initialize(port, baud=9600, uid=1, opts = {})
36
+ Thread.abort_on_exception = true
37
+ @sp = open_serial_port(port, baud, opts)
38
+ @uid = uid
62
39
  end
63
40
 
41
+ # Start server
64
42
  def start
65
43
  @serv = Thread.new do
66
- loop do
67
- req = ''
68
- while req.size == 0
69
- req = @sp.read
70
- end
71
- if req.getbyte(0) == @slave and req[-2,2].unpack('n')[0] == crc16(req[0..-3])
72
- pdu = exec_req(req[1..-1], @coils, @discrete_inputs, @holding_registers, @input_registers)
73
- resp = @slave.chr + pdu
74
- resp << crc16(resp).to_word
75
- @sp.write resp
76
- end
44
+ serv_rtu_requests(@sp) do |msg|
45
+ exec_req(msg[1..-3], @coils, @discrete_inputs, @holding_registers, @input_registers)
77
46
  end
78
47
  end
79
48
  end
80
49
 
50
+ # Stop server
81
51
  def stop
82
52
  Thread.kill(@serv)
83
- @sp.close
53
+ @sp.close
84
54
  end
85
55
 
56
+ # Join server
86
57
  def join
87
58
  @serv.join
88
59
  end
@@ -0,0 +1,59 @@
1
+ # RModBus - free implementation of ModBus protocol in Ruby.
2
+ #
3
+ # Copyright (C) 2011 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
+ # RTU slave implementation
16
+ # @example
17
+ # RTUClient.connect(port, baud, opts) do |cl|
18
+ # cl.with_slave(uid) do |slave|
19
+ # slave.holding_registers[0..100]
20
+ # end
21
+ # end
22
+ #
23
+ # @see RTUClient#open_connection
24
+ # @see Client#with_slave
25
+ # @see Slave
26
+ class RTUSlave < Slave
27
+ include RTU
28
+
29
+ protected
30
+
31
+ # overide method for RTU implamentaion
32
+ # @see Slave#query
33
+ def send_pdu(pdu)
34
+ msg = @uid.chr + pdu
35
+ msg << crc16(msg).to_word
36
+ @io.write msg
37
+
38
+ log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
39
+ end
40
+
41
+ # overide method for RTU implamentaion
42
+ # @see Slave#query
43
+ def read_pdu
44
+ msg = read_rtu_response(@io)
45
+
46
+ log "Rx (#{msg.size} bytes): " + logging_bytes(msg)
47
+
48
+ if msg.getbyte(0) == @uid
49
+ return msg[1..-3] if msg[-2,2].unpack('n')[0] == crc16(msg[0..-3])
50
+ log "Ignore package: don't match CRC"
51
+ else
52
+ log "Ignore package: don't match uid ID"
53
+ end
54
+ loop do
55
+ #waite timeout
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,6 +1,6 @@
1
1
  # RModBus - free implementation of ModBus protocol in Ruby.
2
2
  #
3
- # Copyright (C) 2009 Timin Aleksey
3
+ # Copyright (C) 2009-2011 Timin Aleksey
4
4
  # Copyright (C) 2010 Kelley Reynolds
5
5
  #
6
6
  # This program is free software: you can redistribute it and/or modify
@@ -12,94 +12,30 @@
12
12
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
13
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
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
15
  module ModBus
21
-
16
+ # RTU over TCP client implementation
17
+ # @example
18
+ # RTUViaTCPClient.connect('127.0.0.1', 10002) do |cl|
19
+ # cl.with_slave(uid) do |slave|
20
+ # slave.holding_registers[0..100]
21
+ # end
22
+ # end
23
+ #
24
+ # @see RTUViaTCPClient#open_connection
25
+ # @see Client#initialize
22
26
  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?
27
+ include RTU
28
+ include TCP
29
+
30
+ protected
31
+ # Open TCP\IP connection
32
+ # @see TCP#open_tcp_connection
33
+ def open_connection(ipaddr, port = 10002, opts = {})
34
+ io = open_tcp_connection(ipaddr, port, opts)
72
35
  end
73
36
 
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
37
+ def get_slave(uid, io)
38
+ RTUViaTCPSlave.new(uid, io)
39
+ end
104
40
  end
105
41
  end
@@ -13,102 +13,38 @@
13
13
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
14
  # GNU General Public License for more details.
15
15
 
16
- require 'rmodbus/parsers'
17
16
  require 'gserver'
18
17
 
19
18
  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
19
+ # RTU over TCP server implementation
20
+ # @example
21
+ # srv = RTUViaTCPServer.new(10002, 1)
22
+ # srv.coils = [1,0,1,1]
23
+ # srv.discrete_inputs = [1,1,0,0]
24
+ # srv.holding_registers = [1,2,3,4]
25
+ # srv.input_registers = [1,2,3,4]
26
+ # srv.debug = true
27
+ # srv.start
28
+ class RTUViaTCPServer < GServer
29
+ include Common
30
+ include RTU
31
+ include Server
32
+
33
+ # Init server
34
+ # @param [Integer] port listen port
35
+ # @param [Integer] uid slave device
36
+ def initialize(port = 10002, uid = 1)
37
+ @uid = uid
38
+ super(port)
39
+ end
40
+
41
+ protected
42
+ # Serve requests
43
+ # @param [TCPSocket] io socket
44
+ def serve(io)
45
+ serv_rtu_requests(io) do |msg|
46
+ exec_req(msg[1..-3], @coils, @discrete_inputs, @holding_registers, @input_registers)
47
+ end
48
+ end
49
+ end
114
50
  end
@@ -0,0 +1,58 @@
1
+ # RModBus - free implementation of ModBus protocol in Ruby.
2
+ #
3
+ # Copyright (C) 2011 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
+ # RTU over TCP slave implementation
16
+ # @example
17
+ # RTUViaTCP.connect('127.0.0.1', 10002) do |cl|
18
+ # cl.with_slave(uid) do |slave|
19
+ # slave.holding_registers[0..100]
20
+ # end
21
+ # end
22
+ #
23
+ # @see RTUViaTCPClient#open_connection
24
+ # @see Client#with_slave
25
+ # @see Slave
26
+ class RTUViaTCPSlave < Slave
27
+ include RTU
28
+
29
+ protected
30
+ # overide method for RTU over TCP implamentaion
31
+ # @see Slave#query
32
+ def send_pdu(pdu)
33
+ msg = @uid.chr + pdu
34
+ msg << crc16(msg).to_word
35
+ @io.write msg
36
+
37
+ log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
38
+ end
39
+
40
+ # overide method for RTU over TCP implamentaion
41
+ # @see Slave#query
42
+ def read_pdu
43
+ # Read the response appropriately
44
+ msg = read_rtu_response(@io)
45
+
46
+ log "Rx (#{msg.size} bytes): " + logging_bytes(msg)
47
+ if msg.getbyte(0) == @uid
48
+ return msg[1..-3] if msg[-2,2].unpack('n')[0] == crc16(msg[0..-3])
49
+ log "Ignore package: don't match CRC"
50
+ else
51
+ log "Ignore package: don't match uid ID"
52
+ end
53
+ loop do
54
+ #waite timeout
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,6 +1,7 @@
1
- # RModBus - free implementation of ModBus protocol in purge Ruby.
1
+ # RModBus - free implementation of ModBus protocol in Ruby.
2
2
  #
3
3
  # Copyright (C) 2010 Timin Aleksey
4
+ # Copyright (C) 2010 Kelley Reynolds
4
5
  #
5
6
  # This program is free software: you can redistribute it and/or modify
6
7
  # it under the terms of the GNU General Public License as published by
@@ -13,17 +14,25 @@
13
14
  # GNU General Public License for more details.
14
15
 
15
16
  module ModBus
16
- module Parsers
17
-
17
+ # Module for implementation ModBus server
18
+ module Server
18
19
  Funcs = [1,2,3,4,5,6,15,16]
19
20
 
21
+ attr_accessor :coils, :discrete_inputs, :holding_registers, :input_registers, :uid
22
+ @coils = []
23
+ @discrete_inputs = []
24
+ @holding_registers =[]
25
+ @input_registers = []
26
+
27
+ private
28
+
20
29
  def exec_req(req, coils, discrete_inputs, holding_registers, input_registers)
21
30
  func = req.getbyte(0)
22
-
31
+
23
32
  unless Funcs.include?(func)
24
33
  params = { :err => 1 }
25
34
  end
26
-
35
+
27
36
  case func
28
37
  when 1
29
38
  params = parse_read_func(req, coils)
@@ -47,7 +56,7 @@ module ModBus
47
56
  if params[:err] == 0
48
57
  pdu = func.chr + (params[:quant] * 2).chr + input_registers[params[:addr],params[:quant]].pack('n*')
49
58
  end
50
- when 5
59
+ when 5
51
60
  params = parse_write_coil_func(req)
52
61
  if params[:err] == 0
53
62
  coils[params[:addr]] = params[:val]
@@ -72,24 +81,23 @@ module ModBus
72
81
  pdu = req[0,5]
73
82
  end
74
83
  end
75
-
84
+
76
85
  if params[:err] == 0
77
- pdu
86
+ pdu
78
87
  else
79
88
  pdu = (func | 0x80).chr + params[:err].chr
80
89
  end
81
90
  end
82
91
 
83
- private
84
92
  def parse_read_func(req, field)
85
93
  quant = req[3,2].unpack('n')[0]
86
94
 
87
95
  return { :err => 3} unless quant <= 0x7d
88
-
96
+
89
97
  addr = req[1,2].unpack('n')[0]
90
98
  return { :err => 2 } unless addr + quant <= field.size
91
-
92
- return { :err => 0, :quant => quant, :addr => addr }
99
+
100
+ return { :err => 0, :quant => quant, :addr => addr }
93
101
  end
94
102
 
95
103
  def parse_write_coil_func(req)
@@ -98,9 +106,9 @@ module ModBus
98
106
 
99
107
  val = req[3,2].unpack('n')[0]
100
108
  return { :err => 3 } unless val == 0 or val == 0xff00
101
-
109
+
102
110
  val = 1 if val == 0xff00
103
- return { :err => 0, :addr => addr, :val => val }
111
+ return { :err => 0, :addr => addr, :val => val }
104
112
  end
105
113
 
106
114
  def parse_write_register_func(req)
@@ -109,7 +117,7 @@ module ModBus
109
117
 
110
118
  val = req[3,2].unpack('n')[0]
111
119
 
112
- return { :err => 0, :addr => addr, :val => val }
120
+ return { :err => 0, :addr => addr, :val => val }
113
121
  end
114
122
 
115
123
  def parse_write_multiple_coils_func(req)
@@ -129,5 +137,6 @@ module ModBus
129
137
  end
130
138
  params
131
139
  end
140
+
132
141
  end
133
142
  end