rmodbus-ccutrer 2.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 (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