rmodbus 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,133 @@
1
+ # RModBus - free implementation of ModBus protocol in purge Ruby.
2
+ #
3
+ # Copyright (C) 2010 Timin Aleksey
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ module ModBus
16
+ module Parsers
17
+
18
+ Funcs = [1,2,3,4,5,6,15,16]
19
+
20
+ def exec_req(req, coils, discrete_inputs, holding_registers, input_registers)
21
+ func = req.getbyte(0)
22
+
23
+ unless Funcs.include?(func)
24
+ params = { :err => 1 }
25
+ end
26
+
27
+ case func
28
+ when 1
29
+ params = parse_read_func(req, coils)
30
+ if params[:err] == 0
31
+ val = coils[params[:addr],params[:quant]].pack_to_word
32
+ pdu = func.chr + val.size.chr + val
33
+ end
34
+ when 2
35
+ params = parse_read_func(req, discrete_inputs)
36
+ if params[:err] == 0
37
+ val = discrete_inputs[params[:addr],params[:quant]].pack_to_word
38
+ pdu = func.chr + val.size.chr + val
39
+ end
40
+ when 3
41
+ params = parse_read_func(req, holding_registers)
42
+ if params[:err] == 0
43
+ pdu = func.chr + (params[:quant] * 2).chr + holding_registers[params[:addr],params[:quant]].pack('n*')
44
+ end
45
+ when 4
46
+ params = parse_read_func(req, input_registers)
47
+ if params[:err] == 0
48
+ pdu = func.chr + (params[:quant] * 2).chr + input_registers[params[:addr],params[:quant]].pack('n*')
49
+ end
50
+ when 5
51
+ params = parse_write_coil_func(req)
52
+ if params[:err] == 0
53
+ coils[params[:addr]] = params[:val]
54
+ pdu = req
55
+ end
56
+ when 6
57
+ params = parse_write_register_func(req)
58
+ if params[:err] == 0
59
+ holding_registers[params[:addr]] = params[:val]
60
+ pdu = req
61
+ end
62
+ when 15
63
+ params = parse_write_multiple_coils_func(req)
64
+ if params[:err] == 0
65
+ coils[params[:addr],params[:quant]] = params[:val][0,params[:quant]]
66
+ pdu = req[0,5]
67
+ end
68
+ when 16
69
+ params = parse_write_multiple_registers_func(req)
70
+ if params[:err] == 0
71
+ holding_registers[params[:addr],params[:quant]] = params[:val][0,params[:quant]]
72
+ pdu = req[0,5]
73
+ end
74
+ end
75
+
76
+ if params[:err] == 0
77
+ pdu
78
+ else
79
+ pdu = (func | 0x80).chr + params[:err].chr
80
+ end
81
+ end
82
+
83
+ private
84
+ def parse_read_func(req, field)
85
+ quant = req[3,2].unpack('n')[0]
86
+
87
+ return { :err => 3} unless quant <= 0x7d
88
+
89
+ addr = req[1,2].unpack('n')[0]
90
+ return { :err => 2 } unless addr + quant <= field.size
91
+
92
+ return { :err => 0, :quant => quant, :addr => addr }
93
+ end
94
+
95
+ def parse_write_coil_func(req)
96
+ addr = req[1,2].unpack('n')[0]
97
+ return { :err => 2 } unless addr <= @coils.size
98
+
99
+ val = req[3,2].unpack('n')[0]
100
+ return { :err => 3 } unless val == 0 or val == 0xff00
101
+
102
+ val = 1 if val == 0xff00
103
+ return { :err => 0, :addr => addr, :val => val }
104
+ end
105
+
106
+ def parse_write_register_func(req)
107
+ addr = req[1,2].unpack('n')[0]
108
+ return { :err => 2 } unless addr <= @coils.size
109
+
110
+ val = req[3,2].unpack('n')[0]
111
+
112
+ return { :err => 0, :addr => addr, :val => val }
113
+ end
114
+
115
+ def parse_write_multiple_coils_func(req)
116
+ params = parse_read_func(req, @coils)
117
+
118
+ if params[:err] == 0
119
+ params = {:err => 0, :addr => params[:addr], :quant => params[:quant], :val => req[6,params[:quant]].unpack_bits }
120
+ end
121
+ params
122
+ end
123
+
124
+ def parse_write_multiple_registers_func(req)
125
+ params = parse_read_func(req, @holding_registers)
126
+
127
+ if params[:err] == 0
128
+ params = {:err => 0, :addr => params[:addr], :quant => params[:quant], :val => req[6,params[:quant] * 2].unpack('n*')}
129
+ end
130
+ params
131
+ end
132
+ end
133
+ end
@@ -1,3 +1,18 @@
1
+ # RModBus - free implementation of ModBus protocol in Ruby.
2
+ #
3
+ # Copyright (C) 2009 Timin Aleksey
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ require 'rmodbus/crc16'
15
+
1
16
  begin
2
17
  require 'rubygems'
3
18
  rescue
@@ -7,86 +22,116 @@ require 'serialport'
7
22
  module ModBus
8
23
 
9
24
  class RTUClient < Client
10
-
11
- def initialize(port, rate=9600, slaveaddr=1)
12
- @port = SerialPort.new(port, rate)
13
- @port.read_timeout = 5
14
- @slave = slaveaddr
25
+
26
+ include CRC16
27
+ attr_reader :port, :baud, :slave, :data_bits, :stop_bits, :parity
28
+ attr_accessor :debug
29
+
30
+ # Connect with RTU server
31
+ #
32
+ # port - serial port of connections with RTU server
33
+ #
34
+ # baud - rate sp of connections with RTU server
35
+ #
36
+ # slaveaddr - slave ID of the RTU server
37
+ #
38
+ # Options:
39
+ #
40
+ # :data_bits => from 5 to 8
41
+ #
42
+ # :stop_bits => 1 or 2
43
+ #
44
+ # :parity => NONE, EVEN or ODD
45
+ #
46
+ # RTUClient.connect('/dev/port1') do |cl|
47
+ #
48
+ # put cl.read_holding_registers(0, 10)
49
+ #
50
+ # end
51
+ def self.connect(port, baud=9600, slaveaddr=1, options = {})
52
+ cl = RTUClient.new(port, baud, slaveaddr, options)
53
+ yield cl
54
+ cl.close
55
+ end
56
+
57
+ # Connect with RTU server
58
+ #
59
+ # port - serial port of connections with RTU server
60
+ #
61
+ # baud - rate sp of connections with RTU server
62
+ #
63
+ # data_bits - from 5 to 8
64
+ #
65
+ # stop_bits - 1 or 2
66
+ #
67
+ # parity - NONE, EVEN or ODD
68
+ #
69
+ # slaveaddr - slave ID of the RTU server # Connect with RTU server
70
+ #
71
+ # port - serial port of connections with RTU server
72
+ #
73
+ # baud - rate sp of connections with RTU server
74
+ #
75
+ # slaveaddr - slave ID of the RTU server
76
+ #
77
+ # Options:
78
+ #
79
+ # :data_bits => from 5 to 8
80
+ #
81
+ # :stop_bits => 1 or 2
82
+ #
83
+ # :parity => NONE, EVEN or ODD
84
+ def initialize(port, baud=9600, slaveaddr=1, options = {})
85
+ @port, @baud, @slave = port, baud, slaveaddr
86
+
87
+ @data_bits, @stop_bits, @parity = 8, 1, SerialPort::NONE
88
+
89
+ @data_bits = options[:data_bits] unless options[:data_bits].nil?
90
+ @stop_bits = options[:stop_bits] unless options[:stop_bits].nil?
91
+ @parity = options[:parity] unless options[:parity].nil?
92
+
93
+ @debug = false
94
+
95
+ @sp = SerialPort.new(@port, @baud, @data_bits, @stop_bits, @parity)
96
+ @sp.read_timeout = 5
97
+
15
98
  super()
16
99
  end
17
100
 
101
+ def close
102
+ @sp.close unless @sp.closed?
103
+ end
104
+
105
+ def closed?
106
+ @sp.closed?
107
+ end
108
+
18
109
  protected
19
110
  def send_pdu(pdu)
20
111
  msg = @slave.chr + pdu
21
112
  msg << crc16(msg).to_word
22
- @port.write msg
113
+ @sp.write msg
114
+ if @debug
115
+ STDOUT << "Tx (#{msg.size} bytes): " + logging_bytes(msg) + "\n"
116
+ end
23
117
  end
24
118
 
25
119
  def read_pdu
26
120
  msg = ''
27
- while msg.size == 0 do
28
- msg = @port.read
121
+ while msg.size == 0
122
+ msg = @sp.read
123
+ end
124
+
125
+ if @debug
126
+ STDOUT << "Rx (#{msg.size} bytes): " + logging_bytes(msg) + "\n"
29
127
  end
30
128
 
31
129
  if msg.getbyte(0) == @slave
32
- return msg[1..-3] if msg[-2,2] == crc16(msg[0..-3]).to_word
130
+ return msg[1..-3] if msg[-2,2].unpack('n')[0] == crc16(msg[0..-3])
33
131
  end
34
132
  loop do
35
133
  #waite timeout
36
134
  end
37
135
  end
38
-
39
- def crc16(msg)
40
- crc_lo = 0xff
41
- crc_hi = 0xff
42
-
43
- msg.unpack('c*').each do |byte|
44
- i = crc_hi ^ byte
45
- crc_hi = crc_lo ^ CrcHiTable[i]
46
- crc_lo = CrcLoTable[i]
47
- end
48
-
49
- return ((crc_hi << 8) + crc_lo)
50
- end
51
-
52
- CrcHiTable = [
53
- 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
54
- 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
55
- 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
56
- 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
57
- 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
58
- 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
59
- 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
60
- 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
61
- 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
62
- 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
63
- 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
64
- 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
65
- 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
66
- 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
67
- 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
68
- 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
69
- 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
70
- 0x40]
71
- CrcLoTable = [
72
- 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
73
- 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
74
- 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
75
- 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
76
- 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
77
- 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
78
- 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
79
- 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
80
- 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
81
- 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
82
- 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
83
- 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
84
- 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
85
- 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
86
- 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
87
- 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
88
- 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
89
- 0x40]
90
136
  end
91
-
92
137
  end
@@ -0,0 +1,90 @@
1
+ # RModBus - free implementation of ModBus protocol in Ruby.
2
+ #
3
+ # Copyright (C) 2010 Timin Aleksey
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ require 'rmodbus/parsers'
16
+
17
+ begin
18
+ require 'rubygems'
19
+ rescue
20
+ end
21
+
22
+ require 'serialport'
23
+
24
+ module ModBus
25
+
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
62
+ end
63
+
64
+ def start
65
+ @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
77
+ end
78
+ end
79
+ end
80
+
81
+ def stop
82
+ Thread.kill(@serv)
83
+ @sp.close
84
+ end
85
+
86
+ def join
87
+ @serv.join
88
+ end
89
+ end
90
+ end
@@ -23,12 +23,39 @@ module ModBus
23
23
 
24
24
  include Timeout
25
25
 
26
+ attr_reader :ipaddr, :port, :slave
27
+ attr_accessor :debug
28
+
26
29
  @@transaction = 0
30
+
31
+ # Connect with ModBus server
32
+ #
33
+ # ipaddr - ip of the server
34
+ #
35
+ # port - port TCP connections
36
+ #
37
+ # slaveaddr - slave ID of the server
38
+ #
39
+ # TCPClient.connect('127.0.0.1') do |cl|
40
+ #
41
+ # put cl.read_holding_registers(0, 10)
42
+ #
43
+ # end
44
+ def self.connect(ipaddr, port = 502, slaveaddr = 1)
45
+ cl = TCPClient.new(ipaddr, port, slaveaddr)
46
+ yield cl
47
+ cl.close
48
+ end
27
49
 
28
50
  # Connect with a ModBus server
51
+ #
52
+ # ipaddr - ip of the server
53
+ #
54
+ # port - port TCP connections
55
+ #
56
+ # slaveaddr - slave ID of the server
29
57
  def initialize(ipaddr, port = 502, slaveaddr = 1)
30
58
  @ipaddr, @port = ipaddr, port
31
- super()
32
59
  tried = 0
33
60
  begin
34
61
  timeout(1, ModBusTimeout) do
@@ -40,12 +67,20 @@ module ModBus
40
67
  raise ModBusTimeout.new, 'Timed out attempting to create connection'
41
68
  end
42
69
  @slave = slaveaddr
70
+ @debug = false
71
+ super()
43
72
  end
44
73
 
74
+ # Close TCP connections
45
75
  def close
46
76
  @sock.close unless @sock.closed?
47
77
  end
48
78
 
79
+ # Check TCP connections
80
+ def closed?
81
+ @sock.closed?
82
+ end
83
+
49
84
  def self.transaction
50
85
  @@transaction
51
86
  end
@@ -54,7 +89,12 @@ module ModBus
54
89
  def send_pdu(pdu)
55
90
  @@transaction = 0 if @@transaction.next > 65535
56
91
  @@transaction += 1
57
- @sock.write @@transaction.to_word + "\0\0" + (pdu.size + 1).to_word + @slave.chr + pdu
92
+ msg = @@transaction.to_word + "\0\0" + (pdu.size + 1).to_word + @slave.chr + pdu
93
+ @sock.write msg
94
+
95
+ if debug
96
+ STDOUT << "Tx (#{msg.size} bytes): " + logging_bytes(msg) + "\n"
97
+ end
58
98
  end
59
99
 
60
100
  def read_pdu
@@ -63,7 +103,11 @@ module ModBus
63
103
  tin = header[0,2].unpack('n')[0]
64
104
  raise Errors::ModBusException.new("Transaction number mismatch") unless tin == @@transaction
65
105
  len = header[4,2].unpack('n')[0]
66
- @sock.read(len-1)
106
+ msg = @sock.read(len-1)
107
+ if @debug
108
+ STDOUT << "Rx (#{(header + msg).size} bytes): " + logging_bytes(header + msg) + "\n"
109
+ end
110
+ msg
67
111
  else
68
112
  raise Errors::ModBusException.new("Server did not respond")
69
113
  end