em-modbus 0.1.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 (41) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +373 -0
  4. data/README.md +30 -0
  5. data/Rakefile +15 -0
  6. data/em-modbus.gemspec +26 -0
  7. data/examples/client.rb +33 -0
  8. data/examples/client_batch.rb +47 -0
  9. data/examples/server.rb +43 -0
  10. data/lib/modbus/adu/adu.rb +5 -0
  11. data/lib/modbus/adu/rtu_adu.rb +18 -0
  12. data/lib/modbus/adu/tcp_adu.rb +82 -0
  13. data/lib/modbus/client.rb +64 -0
  14. data/lib/modbus/connection/base.rb +41 -0
  15. data/lib/modbus/connection/connection.rb +7 -0
  16. data/lib/modbus/connection/protocol_data.rb +78 -0
  17. data/lib/modbus/connection/tcp_client.rb +82 -0
  18. data/lib/modbus/connection/tcp_server.rb +36 -0
  19. data/lib/modbus/exceptions.rb +106 -0
  20. data/lib/modbus/modbus.rb +12 -0
  21. data/lib/modbus/pdu/exception.rb +56 -0
  22. data/lib/modbus/pdu/pdu.rb +87 -0
  23. data/lib/modbus/pdu/read_holding_registers.rb +26 -0
  24. data/lib/modbus/pdu/read_input_registers.rb +26 -0
  25. data/lib/modbus/pdu/read_input_status.rb +147 -0
  26. data/lib/modbus/pdu/read_registers.rb +135 -0
  27. data/lib/modbus/pdu/write_multiple_registers.rb +158 -0
  28. data/lib/modbus/register/base.rb +19 -0
  29. data/lib/modbus/register/bit_register.rb +49 -0
  30. data/lib/modbus/register/register.rb +7 -0
  31. data/lib/modbus/register/word_register.rb +30 -0
  32. data/lib/modbus/server.rb +120 -0
  33. data/lib/modbus/transaction/base.rb +25 -0
  34. data/lib/modbus/transaction/client.rb +162 -0
  35. data/lib/modbus/transaction/server.rb +95 -0
  36. data/lib/modbus/transaction/transaction.rb +39 -0
  37. data/lib/modbus/version.rb +7 -0
  38. data/lib/modbus.rb +2 -0
  39. data/spec/adi_spec.rb +16 -0
  40. data/spec/spec_helper.rb +1 -0
  41. metadata +162 -0
@@ -0,0 +1,64 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+ require 'uri'
5
+
6
+ module Modbus
7
+
8
+ class Client
9
+
10
+
11
+ def initialize(uri)
12
+ @base_polling_interval = 1
13
+ @last_poll_time = Time.now
14
+ @uri = URI uri
15
+ end
16
+
17
+
18
+ def connected(conn)
19
+ @conn = conn
20
+ schedule_next_poll
21
+ end
22
+
23
+
24
+ def disconnected(conn)
25
+ reconnect
26
+ end
27
+
28
+
29
+ def connect
30
+ EM.connect @uri.host, @uri.port, Modbus::Connection::TCPClient, self
31
+ end
32
+
33
+
34
+ private
35
+
36
+
37
+ def reconnect
38
+ EM.add_timer(5) { connect }
39
+ end
40
+
41
+
42
+ def schedule_next_poll
43
+ poll_time = Time.now - @last_poll_time
44
+ next_poll_time = [@base_polling_interval - poll_time, @base_polling_interval*0.25].max
45
+
46
+ EM.add_timer(next_poll_time) do
47
+ @last_poll_time = Time.now
48
+ poll
49
+ end
50
+ end
51
+
52
+
53
+ def poll
54
+ # no-op
55
+ end
56
+
57
+
58
+ def transaction(timeout = 2)
59
+ yield Modbus::Transaction::Client.new @conn, timeout
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,41 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+ module Modbus
5
+ module Connection
6
+
7
+ class Base < EM::Connection
8
+
9
+
10
+ def initialize(handler)
11
+ @handler = handler
12
+ reset_buffer
13
+ end
14
+
15
+
16
+ def receive_data(data)
17
+ @buffer << data
18
+ analyze_buffer
19
+
20
+ rescue => e
21
+ # TODO log exception
22
+ # puts e.message
23
+ reset_buffer
24
+ end
25
+
26
+
27
+ def reset_buffer
28
+ @buffer = String.new
29
+ end
30
+
31
+
32
+ def analyze_buffer
33
+ success = transaction_class.recv_adu @buffer, self
34
+ analyze_buffer if success && !@buffer.empty?
35
+ end
36
+
37
+
38
+ end # Base
39
+
40
+ end # Connection
41
+ end # Modbus
@@ -0,0 +1,7 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+ require 'modbus/connection/protocol_data'
5
+ require 'modbus/connection/base'
6
+ require 'modbus/connection/tcp_client'
7
+ require 'modbus/connection/tcp_server'
@@ -0,0 +1,78 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+ module Modbus
6
+
7
+
8
+ # Helper class for dealing with the modbus wire format.
9
+ #
10
+ class ProtocolData < ::Array
11
+
12
+
13
+ # Initializes a new ProtocolData instance. Unpacks a buffer string if given.
14
+ #
15
+ # @param buffer [String, Array] The buffer data. If it's a String, it's automatically unpacked.
16
+ #
17
+ def initialize(buffer = nil)
18
+ case buffer
19
+ when String
20
+ super buffer.unpack('C*')
21
+ when Array
22
+ super buffer
23
+ end
24
+ end
25
+
26
+
27
+ # Shifts two bytes off the from front of the array and interprets them as a word (network byte order).
28
+ #
29
+ # @return [Integer, NilClass] The shifted word or nil if there are not enough bytes.
30
+ #
31
+ def shift_word
32
+ return nil if size < 2
33
+ # self.shift(2).pack('C2').unpack('n').first
34
+ self.slice!(0,2).pack('C2').unpack('n').first
35
+ end
36
+
37
+
38
+ # Interprets a value as a word (network byte order) and pushes two bytes to the end of the array.
39
+ #
40
+ # @param word [Integer] The value to push.
41
+ #
42
+ def push_word(word)
43
+ self.concat [word].pack('n').unpack('C2')
44
+ end
45
+
46
+
47
+ # Shifts one bytes off the from front of the array.
48
+ #
49
+ # @return [Integer] The shifted byte.
50
+ #
51
+ def shift_byte
52
+ # self.shift
53
+ self.slice!(0,1).first
54
+ end
55
+
56
+
57
+ # Interprets a value as a byte (network byte order) and pushes the byte to the end of the array.
58
+ #
59
+ # @param byte [Integer] The value to push.
60
+ #
61
+ def push_byte(byte)
62
+ self.concat [byte].pack('C').unpack('C')
63
+ end
64
+
65
+
66
+ # Converts the array data into a string.
67
+ #
68
+ # @return [String] The data string (frozen).
69
+ #
70
+ def to_buffer
71
+ self.pack('C*').freeze
72
+ end
73
+
74
+ end
75
+
76
+ end # Modbus
77
+
78
+
@@ -0,0 +1,82 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+ require 'rubygems'
5
+ require 'eventmachine'
6
+
7
+ module Modbus
8
+ module Connection
9
+
10
+ class TCPClient < Base
11
+
12
+
13
+ # Initializes a new connection instance.
14
+ #
15
+ # @param handler The managing handler object.
16
+ #
17
+ def initialize(handler)
18
+ super
19
+
20
+ @transaction_ident = 0
21
+ @pending_transactions = []
22
+ end
23
+
24
+
25
+ # EM callback. Called when the TCP connection is sucessfully established.
26
+ #
27
+ def connection_completed
28
+ @handler.connected self
29
+ end
30
+
31
+
32
+ # EM callback. Called when the TCP connection is closed.
33
+ #
34
+ def unbind
35
+ @handler.disconnected self
36
+ end
37
+
38
+
39
+ # Creates a successor transaction identfication in the range of 0..65535
40
+ #
41
+ # @return [Integer] The transaction identification
42
+ #
43
+ def next_transaction_ident
44
+ @transaction_ident = @transaction_ident.succ.modulo 2**16
45
+ end
46
+
47
+
48
+ # Adds Transaction object to the list of tracked transactions
49
+ #
50
+ # @param transaction [Modbus::Transaction] The transaction object to add.
51
+ #
52
+ def track_transaction(transaction)
53
+ # TODO log "exception"
54
+ # puts "Too many pending pending transactions: #{@pending_transactions.size}" if @pending_transactions.size > 100
55
+ @pending_transactions << transaction
56
+ end
57
+
58
+
59
+ # Looks for a matching transaction in the internal store by a transaction ident and returns it if found.
60
+ #
61
+ # @param transaction_ident [Integer] The transaction ident to match.
62
+ # @return [Modbus::Transaction] The found Transaction object.
63
+ #
64
+ def pick_pending_transaction(transaction_ident)
65
+ transaction = @pending_transactions.find { |r| r.transaction_ident == transaction_ident }
66
+ @pending_transactions.delete transaction
67
+ transaction
68
+ end
69
+
70
+
71
+ private
72
+
73
+
74
+ def transaction_class
75
+ Modbus::Transaction::Client
76
+ end
77
+
78
+
79
+ end # TCPClient
80
+
81
+ end # Connection
82
+ end # Modbus
@@ -0,0 +1,36 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+ require 'forwardable'
5
+
6
+ module Modbus
7
+ module Connection
8
+
9
+ class TCPServer < Base
10
+ extend Forwardable
11
+
12
+ def_delegator :@handler, :read_registers, :read_registers
13
+ def_delegator :@handler, :write_registers, :write_registers
14
+
15
+
16
+ def post_init
17
+ @handler.client_connected signature
18
+ end
19
+
20
+
21
+ def unbind
22
+ @handler.client_disconnected signature
23
+ end
24
+
25
+
26
+ private
27
+
28
+
29
+ def transaction_class
30
+ Modbus::Transaction::Server
31
+ end
32
+
33
+ end # TCPServer
34
+
35
+ end # Connection
36
+ end # Modbus
@@ -0,0 +1,106 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+ module Modbus
6
+ ClientError = Class.new StandardError
7
+ ModbusError = Class.new StandardError
8
+
9
+ class ModbusError < StandardError
10
+ attr_reader :code
11
+
12
+ def initialize(msg)
13
+ super
14
+ @code = self.class::CODE
15
+ end
16
+ end
17
+
18
+ class IllegalFunction < ModbusError
19
+ CODE = 0x01
20
+
21
+ def initialize(msg = 'ILLEGAL FUNCTION')
22
+ super
23
+ end
24
+ end
25
+
26
+ class IllegalDataAddress < ModbusError
27
+ CODE = 0x02
28
+
29
+ def initialize(msg = 'ILLEGAL DATA ADDRESS')
30
+ super
31
+ end
32
+ end
33
+
34
+ class IllegalDataValue < ModbusError
35
+ CODE = 0x03
36
+
37
+ def initialize(msg = 'ILLEGAL DATA VALUE')
38
+ super
39
+ end
40
+ end
41
+
42
+ class ServerDeviceFailure < ModbusError
43
+ CODE = 0x04
44
+
45
+ def initialize(msg = 'SERVER DEVICE FAILURE')
46
+ super
47
+ end
48
+ end
49
+
50
+ class Acknowledge < ModbusError
51
+ CODE = 0x05
52
+
53
+ def initialize(msg = 'ACKNOWLEDGE')
54
+ super
55
+ end
56
+ end
57
+
58
+ class ServerDeviceBusy < ModbusError
59
+ CODE = 0x06
60
+
61
+ def initialize(msg = 'SERVER DEVICE BUSY')
62
+ super
63
+ end
64
+ end
65
+
66
+ class MemoryParityError < ModbusError
67
+ CODE = 0x08
68
+
69
+ def initialize(msg = 'MEMORY PARITY ERROR')
70
+ super
71
+ end
72
+ end
73
+
74
+ class GatewayPathUnavailable < ModbusError
75
+ CODE = 0x0A
76
+
77
+ def initialize(msg = 'GATEWAY PATH UNAVAILABLE')
78
+ super
79
+ end
80
+ end
81
+
82
+ class GatewayTargetDeviceFailedToRespond < ModbusError
83
+ CODE = 0x0B
84
+
85
+ def initialize(msg = 'GATEWAY TARGET DEVICE FAILED TO RESPOND')
86
+ super
87
+ end
88
+ end
89
+
90
+ def self.find_exception(code)
91
+ exceptions = [
92
+ IllegalFunction,
93
+ IllegalDataAddress,
94
+ IllegalDataValue,
95
+ ServerDeviceFailure,
96
+ Acknowledge,
97
+ ServerDeviceBusy,
98
+ MemoryParityError,
99
+ GatewayPathUnavailable,
100
+ GatewayTargetDeviceFailedToRespond
101
+ ]
102
+
103
+ exceptions.find { |e| e::CODE == code } || RuntimeError
104
+ end
105
+
106
+ end
@@ -0,0 +1,12 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+ require 'modbus/exceptions'
6
+ require 'modbus/pdu/pdu'
7
+ require 'modbus/adu/adu'
8
+ require 'modbus/transaction/transaction'
9
+ require 'modbus/connection/connection'
10
+ require 'modbus/register/register'
11
+ require 'modbus/client'
12
+ require 'modbus/server'
@@ -0,0 +1,56 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+ module Modbus
6
+
7
+ class PDU
8
+
9
+ # PDU for modbus exception (response message)
10
+ #
11
+ class Exception < PDU
12
+ attr_accessor :exception_code
13
+
14
+
15
+ def self.create(func_code, error)
16
+ obj = self.new nil, func_code + 0x80
17
+ obj.exception_code = error.code
18
+ obj
19
+ end
20
+
21
+
22
+ # Decodes a PDU from protocol data.
23
+ #
24
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
25
+ #
26
+ def decode(data)
27
+ @exception_code = data.shift_byte
28
+ end
29
+
30
+
31
+ # Encodes a PDU into protocol data.
32
+ #
33
+ # @return [Modbus::ProtocolData] The protocol data representation of this object.
34
+ #
35
+ def encode
36
+ data = super
37
+ data.push_byte @exception_code
38
+ data
39
+ end
40
+
41
+
42
+ # Returns the length of the PDU in bytes.
43
+ #
44
+ # @return [Integer] The length.
45
+ #
46
+ def length
47
+ 2
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+
54
+ end # Modbus
55
+
56
+
@@ -0,0 +1,87 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+ require 'modbus/pdu/exception'
5
+ require 'modbus/pdu/read_input_status'
6
+ require 'modbus/pdu/read_registers'
7
+ require 'modbus/pdu/read_input_registers'
8
+ require 'modbus/pdu/read_holding_registers'
9
+ require 'modbus/pdu/write_multiple_registers'
10
+
11
+ module Modbus
12
+
13
+
14
+ # Base class modelling a Modbus PDU (Protocol Data Unit)
15
+ #
16
+ class PDU
17
+
18
+ # Maps the Modbus function code to the corresponding class (for request messages)
19
+ REQ_PDU_MAP = {}
20
+ [
21
+ ReadInputStatusRequest,
22
+ ReadInputRegistersRequest,
23
+ ReadHoldingRegistersRequest,
24
+ WriteMultipleRegistersRequest
25
+ ].each { |klass| REQ_PDU_MAP[klass::FUNC_CODE] = klass }
26
+
27
+
28
+ # Maps the Modbus function code to the corresponding class (for response messages)
29
+ RSP_PDU_MAP = {}
30
+ [
31
+ ReadInputStatusResponse,
32
+ ReadInputRegistersResponse,
33
+ ReadHoldingRegistersResponse,
34
+ WriteMultipleRegistersResponse
35
+ ].each { |klass| RSP_PDU_MAP[klass::FUNC_CODE] = klass }
36
+
37
+
38
+ attr_reader :creation_time, :func_code
39
+
40
+
41
+ # Factory method for creating PDUs. Decodes a PDU from protocol data and returns a new PDU instance.
42
+ #
43
+ # @param type [Symbol] The type of PDU which should be created. Must be :request or :response.
44
+ # @param func_code [Integer] The modbus function code of the PDU
45
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
46
+ # @return [Modbus::PDU] The created PDU instance.
47
+ #
48
+ def self.create(type, func_code, data)
49
+ map = { :request => REQ_PDU_MAP, :response => RSP_PDU_MAP }[type]
50
+ fail ArgumentError, "Type is expected to be :request or :response, got #{type}" unless map
51
+
52
+ # 0x80 is the offset in case of a modbus exception
53
+ klass = func_code > 0x80 ? PDU::Exception : map[func_code]
54
+ fail IllegalFunction, "Unknown function code 0x#{func_code.to_s(16)}" if klass.nil?
55
+
56
+ klass.new data, func_code
57
+ end
58
+
59
+
60
+ # Initializes a new PDU instance. Decodes from protocol data if given.
61
+ #
62
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
63
+ # @param func_code [Fixnum] Modbus function code.
64
+ #
65
+ def initialize(data = nil, func_code = nil)
66
+ @creation_time = Time.now.utc
67
+ @func_code = func_code || self.class::FUNC_CODE
68
+
69
+ self.decode data if data
70
+ end
71
+
72
+
73
+ # Encodes a PDU into protocol data.
74
+ #
75
+ # @return [Modbus::ProtocolData] The protocol data representation of this object.
76
+ #
77
+ def encode
78
+ data = ProtocolData.new
79
+ data.push_byte @func_code
80
+ data
81
+ end
82
+
83
+ end
84
+
85
+ end # Modbus
86
+
87
+
@@ -0,0 +1,26 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+ module Modbus
6
+
7
+ class PDU
8
+
9
+ # PDU for modbus function "read holding register" (request message)
10
+ #
11
+ class ReadHoldingRegistersRequest < ReadRegistersRequest
12
+ FUNC_CODE = 0x03
13
+ end
14
+
15
+
16
+ # PDU for modbus function "read holding register" (response message)
17
+ #
18
+ class ReadHoldingRegistersResponse < ReadRegistersResponse
19
+ FUNC_CODE = 0x03
20
+ end
21
+
22
+ end
23
+
24
+ end # Modbus
25
+
26
+
@@ -0,0 +1,26 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+ module Modbus
6
+
7
+ class PDU
8
+
9
+ # PDU for modbus function "read input register" (request message)
10
+ #
11
+ class ReadInputRegistersRequest < ReadRegistersRequest
12
+ FUNC_CODE = 0x04
13
+ end
14
+
15
+
16
+ # PDU for modbus function "read input register" (response message)
17
+ #
18
+ class ReadInputRegistersResponse < ReadRegistersResponse
19
+ FUNC_CODE = 0x04
20
+ end
21
+
22
+ end
23
+
24
+ end # Modbus
25
+
26
+