rmodbus-ccutrer 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/NEWS.md +180 -0
  3. data/README.md +115 -0
  4. data/Rakefile +29 -0
  5. data/examples/perfomance_rtu.rb +56 -0
  6. data/examples/perfomance_rtu_via_tcp.rb +55 -0
  7. data/examples/perfomance_tcp.rb +55 -0
  8. data/examples/simple-xpca-gateway.rb +84 -0
  9. data/examples/use_rtu_via_tcp_modbus.rb +22 -0
  10. data/examples/use_tcp_modbus.rb +23 -0
  11. data/lib/rmodbus.rb +21 -0
  12. data/lib/rmodbus/client.rb +94 -0
  13. data/lib/rmodbus/client/slave.rb +345 -0
  14. data/lib/rmodbus/debug.rb +25 -0
  15. data/lib/rmodbus/errors.rb +42 -0
  16. data/lib/rmodbus/ext.rb +85 -0
  17. data/lib/rmodbus/options.rb +6 -0
  18. data/lib/rmodbus/proxy.rb +41 -0
  19. data/lib/rmodbus/rtu.rb +122 -0
  20. data/lib/rmodbus/rtu_client.rb +43 -0
  21. data/lib/rmodbus/rtu_server.rb +48 -0
  22. data/lib/rmodbus/rtu_slave.rb +48 -0
  23. data/lib/rmodbus/rtu_via_tcp_server.rb +35 -0
  24. data/lib/rmodbus/server.rb +246 -0
  25. data/lib/rmodbus/server/slave.rb +16 -0
  26. data/lib/rmodbus/sp.rb +36 -0
  27. data/lib/rmodbus/tcp.rb +31 -0
  28. data/lib/rmodbus/tcp_client.rb +25 -0
  29. data/lib/rmodbus/tcp_server.rb +67 -0
  30. data/lib/rmodbus/tcp_slave.rb +55 -0
  31. data/lib/rmodbus/version.rb +3 -0
  32. data/spec/client_spec.rb +88 -0
  33. data/spec/exception_spec.rb +120 -0
  34. data/spec/ext_spec.rb +52 -0
  35. data/spec/logging_spec.rb +89 -0
  36. data/spec/proxy_spec.rb +74 -0
  37. data/spec/read_rtu_response_spec.rb +92 -0
  38. data/spec/response_mismach_spec.rb +163 -0
  39. data/spec/rtu_client_spec.rb +86 -0
  40. data/spec/rtu_server_spec.rb +31 -0
  41. data/spec/rtu_via_tcp_client_spec.rb +76 -0
  42. data/spec/rtu_via_tcp_server_spec.rb +89 -0
  43. data/spec/slave_spec.rb +55 -0
  44. data/spec/spec_helper.rb +54 -0
  45. data/spec/tcp_client_spec.rb +88 -0
  46. data/spec/tcp_server_spec.rb +158 -0
  47. metadata +206 -0
@@ -0,0 +1,25 @@
1
+ require 'time'
2
+
3
+ module ModBus
4
+ module Debug
5
+ attr_accessor :debug, :raise_exception_on_mismatch,
6
+ :read_retries, :read_retry_timeout
7
+
8
+
9
+ private
10
+ # Put log message on standard output
11
+ # @param [String] msg message for log
12
+ def log(msg)
13
+ $stdout.puts "#{Time.now.utc.iso8601(2)} #{msg}" if @debug
14
+ end
15
+
16
+ # Convert string of byte to string for log
17
+ # @example
18
+ # logging_bytes("\x1\xa\x8") => "[01][0a][08]"
19
+ # @param [String] msg input string
20
+ # @return [String] readable string of bytes
21
+ def logging_bytes(msg)
22
+ msg.unpack("H*").first.gsub(/\X{2}/, "[\\0]")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ module ModBus
2
+ module Errors
3
+ class ProxyException < StandardError
4
+ end
5
+
6
+ class ModBusException < StandardError
7
+ end
8
+
9
+ class IllegalFunction < ModBusException
10
+ end
11
+
12
+ class IllegalDataAddress < ModBusException
13
+ end
14
+
15
+ class IllegalDataValue < ModBusException
16
+ end
17
+
18
+ class SlaveDeviceFailure < ModBusException
19
+ end
20
+
21
+ class Acknowledge < ModBusException
22
+ end
23
+
24
+ class SlaveDeviceBus < ModBusException
25
+ end
26
+
27
+ class MemoryParityError < ModBusException
28
+ end
29
+
30
+ class ModBusTimeout < ModBusException
31
+ end
32
+
33
+ class ResponseMismatch < ModBusException
34
+ attr_reader :request, :response
35
+ def initialize(msg, request, response)
36
+ super(msg)
37
+ @request = request
38
+ @response = response
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,85 @@
1
+ class String
2
+
3
+ if RUBY_VERSION < "1.9"
4
+ def getbyte(index)
5
+ self[index].to_i
6
+ end
7
+ end
8
+
9
+ def unpack_bits
10
+ array_bit = []
11
+ self.unpack('b*')[0].each_char do |c|
12
+ array_bit << c.to_i
13
+ end
14
+ array_bit
15
+ end
16
+
17
+ # Get word by index
18
+ # @param [Integer] i index first bytes of word
19
+ # @return unpacked word
20
+ def getword(i)
21
+ self[i,2].unpack('n')[0]
22
+ end
23
+ end
24
+
25
+ class Integer
26
+
27
+ # Shortcut or turning an integer into a word
28
+ def to_word
29
+ [self].pack('n')
30
+ end
31
+
32
+ end
33
+
34
+ class Array
35
+
36
+ # Given an array of 16bit Fixnum, we turn it into 32bit Int in big-endian order, halving the size
37
+ def to_32f
38
+ raise "Array requires an even number of elements to pack to 32bits: was #{self.size}" unless self.size.even?
39
+ self.each_slice(2).map { |(lsb, msb)| [msb, lsb].pack('n*').unpack('g')[0] }
40
+ end
41
+
42
+ # Given an array of 16bit Fixnum, we turn it into 32bit Int in little-endian order, halving the size
43
+ def to_32f_le
44
+ raise "Array requires an even number of elements to pack to 32bits: was #{self.size}" unless self.size.even?
45
+ self.each_slice(2).map { |(lsb, msb)| [lsb, msb].pack('n*').unpack('g')[0] }
46
+ end
47
+
48
+ # Given an array of 32bit Floats, we turn it into an array of 16bit Fixnums, doubling the size
49
+ def from_32f
50
+ self.pack('g*').unpack('n*').each_slice(2).map { |arr| arr.reverse }.flatten
51
+ end
52
+
53
+ # Given an array of 16bit Fixnum, we turn it into 32bit Float in big-endian order, halving the size
54
+ def to_32i
55
+ raise "Array requires an even number of elements to pack to 32bits: was #{self.size}" unless self.size.even?
56
+ self.each_slice(2).map { |(lsb, msb)| [msb, lsb].pack('n*').unpack('N')[0] }
57
+ end
58
+
59
+ # Given an array of 32bit Fixnum, we turn it into an array of 16bit fixnums, doubling the size
60
+ def from_32i
61
+ self.pack('N*').unpack('n*').each_slice(2).map { |arr| arr.reverse }.flatten
62
+ end
63
+
64
+ def pack_to_word
65
+ word = 0
66
+ s = ""
67
+ mask = 0x01
68
+
69
+ self.each do |bit|
70
+ word |= mask if bit > 0
71
+ mask <<= 1
72
+ if mask == 0x100
73
+ mask = 0x01
74
+ s << word.chr
75
+ word = 0
76
+ end
77
+ end
78
+ unless mask == 0x01
79
+ s << word.chr
80
+ else
81
+ s
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,6 @@
1
+ module ModBus
2
+ module Options
3
+ attr_accessor :raise_exception_on_mismatch,
4
+ :read_retries, :read_retry_timeout
5
+ end
6
+ end
@@ -0,0 +1,41 @@
1
+ module ModBus
2
+ # Given a slave and a type of operation, execute a single or multiple read using hash syntax
3
+ class ReadOnlyProxy
4
+ # Initialize a proxy for a slave and a type of operation
5
+ def initialize(slave, type)
6
+ @slave, @type = slave, type
7
+ end
8
+
9
+ # Read single or multiple values from a modbus slave depending on whether a Fixnum or a Range was given.
10
+ # Note that in the case of multiples, a pluralized version of the method is sent to the slave
11
+ def [](key)
12
+ if key.instance_of?(0.class)
13
+ @slave.send("read_#{@type}", key)
14
+ elsif key.instance_of?(Range)
15
+ @slave.send("read_#{@type}s", key.first, key.count)
16
+ else
17
+ raise ModBus::Errors::ProxyException, "Invalid argument, must be integer or range. Was #{key.class}"
18
+ end
19
+ end
20
+ end
21
+
22
+ class ReadWriteProxy < ReadOnlyProxy
23
+ # Write single or multiple values to a modbus slave depending on whether a Fixnum or a Range was given.
24
+ # Note that in the case of multiples, a pluralized version of the method is sent to the slave. Also when
25
+ # writing multiple values, the number of elements must match the number of registers in the range or an exception is raised
26
+ def []=(key, val)
27
+ if key.instance_of?(0.class)
28
+ @slave.send("write_#{@type}", key, val)
29
+ elsif key.instance_of?(Range)
30
+ if key.count != val.size
31
+ raise ModBus::Errors::ProxyException, "The size of the range must match the size of the values (#{key.count} != #{val.size})"
32
+ end
33
+
34
+ @slave.send("write_#{@type}s", key.first, val)
35
+ else
36
+ raise ModBus::Errors::ProxyException, "Invalid argument, must be integer or range. Was #{key.class}"
37
+ end
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,122 @@
1
+ require 'digest/crc16_modbus'
2
+ require 'io/wait'
3
+
4
+ module ModBus
5
+ module RTU
6
+ private
7
+
8
+ # We have to read specific amounts of numbers of bytes from the network depending on the function code and content
9
+ def read_rtu_response(io)
10
+ # Read the slave_id and function code
11
+ msg = read(io, 2)
12
+ log logging_bytes(msg)
13
+
14
+ function_code = msg.getbyte(1)
15
+ case function_code
16
+ when 1,2,3,4 then
17
+ # read the third byte to find out how much more
18
+ # we need to read + CRC
19
+ msg += read(io, 1)
20
+ msg += read(io, msg.getbyte(2)+2)
21
+ when 5,6,15,16 then
22
+ # We just read in an additional 6 bytes
23
+ msg += read(io, 6)
24
+ when 22 then
25
+ msg += read(io, 8)
26
+ when 0x80..0xff then
27
+ msg += read(io, 3)
28
+ else
29
+ raise ModBus::Errors::IllegalFunction, "Illegal function: #{function_code}"
30
+ end
31
+ end
32
+
33
+ def clean_input_buff
34
+ # empty the input buffer
35
+ if @io.class.public_method_defined? :flush_input
36
+ @io.flush_input
37
+ else
38
+ @io.flush
39
+ end
40
+ end
41
+
42
+ def read(io, len)
43
+ result = ""
44
+ loop do
45
+ this_iter = io.read(len - result.length)
46
+ result.concat(this_iter) if this_iter
47
+ return result if result.length == len
48
+ io.wait_readable
49
+ end
50
+ end
51
+
52
+ def read_rtu_request(io)
53
+ # Every message is a minimum of 4 bytes (slave id, function code, crc16)
54
+ msg = read(io, 4)
55
+
56
+ # If msg is nil, then our client never sent us anything and it's time to disconnect
57
+ return if msg.nil?
58
+
59
+ loop do
60
+ offset = 0
61
+ crc = msg[-2..-1].unpack("S<").first
62
+
63
+ # scan the bytestream for a valid CRC
64
+ loop do
65
+ break if offset >= msg.length - 3
66
+ calculated_crc = Digest::CRC16Modbus.checksum(msg[offset..-3])
67
+ if crc == calculated_crc
68
+ is_response = (msg.getbyte(offset + 1) & 0x80 == 0x80) ||
69
+ (msg.getbyte(offset) == @last_req_uid &&
70
+ msg.getbyte(offset + 1) == @last_req_func &&
71
+ @last_req_timestamp && Time.now.to_f - @last_req_timestamp < 5)
72
+
73
+ params = is_response ? parse_response(msg.getbyte(offset + 1), msg[(offset + 1)..-3]) :
74
+ parse_request(msg.getbyte(offset + 1), msg[(offset + 1)..-3])
75
+
76
+ unless params.nil?
77
+ if is_response
78
+ @last_req_uid = @last_req_func = @last_req_timestamp = nil
79
+ else
80
+ @last_req_uid = msg.getbyte(offset)
81
+ @last_req_func = msg.getbyte(offset + 1)
82
+ @last_req_timestamp = Time.now.to_f
83
+ end
84
+ log "Server RX discarding #{offset} bytes: #{logging_bytes(msg[0...offset])}" if offset != 0
85
+ log "Server RX (#{msg.size - offset} bytes): #{logging_bytes(msg[offset..-1])}"
86
+ return [msg.getbyte(offset), msg.getbyte(offset + 1), params, msg[offset + 1..-3], is_response]
87
+ end
88
+ end
89
+ offset += 1
90
+ end
91
+
92
+ msg.concat(read(io, 1))
93
+ # maximum message size is 256, so that's as far as we have to
94
+ # be able to see at once
95
+ msg = msg[1..-1] if msg.length > 256
96
+ end
97
+ end
98
+
99
+ def serve(io)
100
+ loop do
101
+ # read the RTU message
102
+ uid, func, params, pdu, is_response = read_rtu_request(io)
103
+
104
+ next if uid.nil?
105
+
106
+ pdu = exec_req(uid, func, params, pdu, is_response: is_response)
107
+ next unless pdu
108
+
109
+ @last_req_uid = @last_req_func = @last_req_timestamp = nil
110
+ resp = uid.chr + pdu
111
+ resp << [crc16(resp)].pack("S<")
112
+ log "Server TX (#{resp.size} bytes): #{logging_bytes(resp)}"
113
+ io.write resp
114
+ end
115
+ end
116
+
117
+ # Calc CRC16 for massage
118
+ def crc16(msg)
119
+ Digest::CRC16Modbus.checksum(msg)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,43 @@
1
+ module ModBus
2
+ # RTU client implementation
3
+ # @example
4
+ # RTUClient.connect('/dev/ttyS1', 9600) do |cl|
5
+ # cl.with_slave(uid) do |slave|
6
+ # slave.holding_registers[0..100]
7
+ # end
8
+ # end
9
+ #
10
+ # @example
11
+ # RTUClient.connect('127.0.0.1', 10002) do |cl|
12
+ # cl.with_slave(uid) do |slave|
13
+ # slave.holding_registers[0..100]
14
+ # end
15
+ # end
16
+ #
17
+ # @see TCP#open_tcp_connection
18
+ # @see SP#open_serial_port
19
+ # @see Client#initialize
20
+ class RTUClient < Client
21
+ include RTU
22
+ include SP
23
+ include TCP
24
+
25
+ protected
26
+ # Open serial port
27
+ def open_connection(port_or_ipaddr, arg = nil, opts = {})
28
+ if port_or_ipaddr.is_a?(IO) || port_or_ipaddr.respond_to?(:read)
29
+ port_or_ipaddr
30
+ elsif File.exist?(port_or_ipaddr) || port_or_ipaddr.start_with?('/dev') || port_or_ipaddr.start_with?('COM')
31
+ arg ||= 9600
32
+ open_serial_port(port_or_ipaddr, arg, opts)
33
+ else
34
+ arg ||= 10002
35
+ open_tcp_connection(port_or_ipaddr, arg, opts)
36
+ end
37
+ end
38
+
39
+ def get_slave(uid, io)
40
+ RTUSlave.new(uid, io)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ module ModBus
2
+ # RTU server implementation
3
+ # @example
4
+ # srv = RTUServer.new('/dev/ttyS1', 9600)
5
+ # slave = srv.with_slave(1)
6
+ # slave.coils = [1,0,1,1]
7
+ # slave.discrete_inputs = [1,1,0,0]
8
+ # slave.holding_registers = [1,2,3,4]
9
+ # slave.input_registers = [1,2,3,4]
10
+ # srv.debug = true
11
+ # srv.start
12
+ class RTUServer
13
+ include Debug
14
+ include Server
15
+ include RTU
16
+ include SP
17
+
18
+ # Init RTU server
19
+ # @param [Integer] uid slave device
20
+ # @see SP#open_serial_port
21
+ def initialize(port, baud=9600, opts = {})
22
+ Thread.abort_on_exception = true
23
+ if port.is_a?(IO) || port.respond_to?(:read)
24
+ @sp = port
25
+ else
26
+ @sp = open_serial_port(port, baud, opts)
27
+ end
28
+ end
29
+
30
+ # Start server
31
+ def start
32
+ @serv = Thread.new do
33
+ serve(@sp)
34
+ end
35
+ end
36
+
37
+ # Stop server
38
+ def stop
39
+ Thread.kill(@serv)
40
+ @sp.close
41
+ end
42
+
43
+ # Join server
44
+ def join
45
+ @serv.join
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ module ModBus
2
+ # RTU slave implementation
3
+ # @example
4
+ # RTUClient.connect(port, baud, opts) do |cl|
5
+ # cl.with_slave(uid) do |slave|
6
+ # slave.holding_registers[0..100]
7
+ # end
8
+ # end
9
+ #
10
+ # @see RTUClient#open_connection
11
+ # @see Client#with_slave
12
+ # @see Slave
13
+ class RTUSlave < Client::Slave
14
+ include RTU
15
+
16
+ private
17
+ # overide method for RTU implamentaion
18
+ # @see Slave#query
19
+ def send_pdu(pdu)
20
+ msg = @uid.chr + pdu
21
+ msg << [crc16(msg)].pack("S<")
22
+
23
+ clean_input_buff
24
+ @io.write msg
25
+
26
+ log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
27
+ end
28
+
29
+ # overide method for RTU implamentaion
30
+ # @see Slave#query
31
+ def read_pdu
32
+ msg = read_rtu_response(@io)
33
+
34
+ log "Rx (#{msg.size} bytes): " + logging_bytes(msg)
35
+
36
+ if msg.getbyte(0) == @uid
37
+ return msg[1..-3] if msg[-2,2].unpack('S<')[0] == crc16(msg[0..-3])
38
+ log "Ignore package: don't match CRC"
39
+ else
40
+ log "Ignore package: don't match uid ID"
41
+ end
42
+ loop do
43
+ #waite timeout
44
+ sleep(0.1)
45
+ end
46
+ end
47
+ end
48
+ end