rmodbus 0.3.1 → 0.4.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.
@@ -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