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,147 @@
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 status" (request message)
10
+ #
11
+ class ReadInputStatusRequest < PDU
12
+ FUNC_CODE = 0x02
13
+
14
+ attr_accessor :start_addr, :bit_count
15
+
16
+
17
+ # Initializes a new PDU instance. Decodes from protocol data if given.
18
+ #
19
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
20
+ #
21
+ def initialize(data = nil, func_code = nil)
22
+ @start_addr = 0
23
+ @bit_count = 0
24
+ super
25
+ end
26
+
27
+
28
+ # Decodes a PDU from protocol data.
29
+ #
30
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
31
+ #
32
+ def decode(data)
33
+ @start_addr = data.shift_word
34
+ @bit_count = data.shift_word
35
+ end
36
+
37
+
38
+ # Encodes a PDU into protocol data.
39
+ #
40
+ # @return [Modbus::ProtocolData] The protocol data representation of this object.
41
+ #
42
+ def encode
43
+ data = super
44
+ data.push_word @start_addr
45
+ data.push_word @bit_count
46
+ data
47
+ end
48
+
49
+
50
+ # Returns the length of the PDU in bytes.
51
+ #
52
+ # @return [Integer] The length.
53
+ #
54
+ def length
55
+ 5
56
+ end
57
+
58
+
59
+ # Validates the PDU. Raises exceptions if validation fails.
60
+ #
61
+ def validate
62
+ fail ClientError, "Register count must be in (1..127), got '#{@bit_count.inspect}'" unless (1..127).include?(@bit_count)
63
+ end
64
+
65
+ end
66
+
67
+
68
+ # PDU for modbus function "read input status" (response message)
69
+ #
70
+ class ReadInputStatusResponse < PDU
71
+ FUNC_CODE = 0x02
72
+
73
+ attr_accessor :bit_values
74
+
75
+
76
+ # Initializes a new PDU instance. Decodes from protocol data if given.
77
+ #
78
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
79
+ #
80
+ def initialize(data = nil, func_code = nil)
81
+ @bit_values = []
82
+ super
83
+ end
84
+
85
+
86
+ # Decodes a PDU from protocol data.
87
+ #
88
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
89
+ #
90
+ def decode(data)
91
+ byte_count = data.shift_byte
92
+ byte_count.times do
93
+ byte = data.shift_byte
94
+
95
+ 8.times do |bit|
96
+ @bit_values.push byte[bit] == 1
97
+ end
98
+ end
99
+ end
100
+
101
+
102
+ # Encodes a PDU into protocol data.
103
+ #
104
+ # @return [Modbus::ProtocolData] The protocol data representation of this object.
105
+ #
106
+ def encode
107
+ data = super
108
+ data.push_byte byte_count
109
+ @bit_values.each do |value|
110
+ data.push_byte value
111
+ end
112
+ data
113
+ end
114
+
115
+
116
+ # Returns the length of the register values in bytes.
117
+ #
118
+ # @return [Integer] The length.
119
+ #
120
+ def byte_count
121
+ @bit_values.size
122
+ end
123
+
124
+
125
+ # Returns the length of the PDU in bytes.
126
+ #
127
+ # @return [Integer] The length.
128
+ #
129
+ def length
130
+ # +1 for func_code, +1 for byte_count
131
+ byte_count + 2
132
+ end
133
+
134
+
135
+ # Validates the PDU. Raises exceptions if validation fails.
136
+ #
137
+ def validate
138
+
139
+ end
140
+
141
+ end
142
+
143
+ end
144
+
145
+ end # Modbus
146
+
147
+
@@ -0,0 +1,135 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+ module Modbus
6
+
7
+ class PDU
8
+
9
+ # Base class PDU for modbus function "read holding register" (request message)
10
+ #
11
+ class ReadRegistersRequest < PDU
12
+ attr_accessor :start_addr, :reg_count
13
+
14
+
15
+ # Initializes a new PDU instance. Decodes from protocol data if given.
16
+ #
17
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
18
+ #
19
+ def initialize(data = nil, func_code = nil)
20
+ @start_addr = 0
21
+ @reg_count = 0
22
+ super
23
+ end
24
+
25
+
26
+ # Decodes a PDU from protocol data.
27
+ #
28
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
29
+ #
30
+ def decode(data)
31
+ @start_addr = data.shift_word
32
+ @reg_count = data.shift_word
33
+ end
34
+
35
+
36
+ # Encodes a PDU into protocol data.
37
+ #
38
+ # @return [Modbus::ProtocolData] The protocol data representation of this object.
39
+ #
40
+ def encode
41
+ data = super
42
+ data.push_word @start_addr
43
+ data.push_word @reg_count
44
+ data
45
+ end
46
+
47
+
48
+ # Returns the length of the PDU in bytes.
49
+ #
50
+ # @return [Integer] The length.
51
+ #
52
+ def length
53
+ 5
54
+ end
55
+
56
+
57
+ # Validates the PDU. Raises exceptions if validation fails.
58
+ #
59
+ def validate
60
+ fail ClientError, "Register count must be in (1..127), got '#{@reg_count.inspect}'" unless (1..127).include?(@reg_count)
61
+ end
62
+
63
+ end
64
+
65
+
66
+ # PDU for modbus function "read holding register" (response message)
67
+ #
68
+ class ReadRegistersResponse < PDU
69
+ attr_accessor :reg_values
70
+
71
+
72
+ # Initializes a new PDU instance. Decodes from protocol data if given.
73
+ #
74
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
75
+ #
76
+ def initialize(data = nil, func_code = nil)
77
+ @reg_values = []
78
+ super
79
+ end
80
+
81
+
82
+ # Decodes a PDU from protocol data.
83
+ #
84
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
85
+ #
86
+ def decode(data)
87
+ byte_count = data.shift_byte
88
+ byte_count.div(2).times { @reg_values.push data.shift_word }
89
+ end
90
+
91
+
92
+ # Encodes a PDU into protocol data.
93
+ #
94
+ # @return [Modbus::ProtocolData] The protocol data representation of this object.
95
+ #
96
+ def encode
97
+ data = super
98
+ data.push_byte byte_count
99
+ @reg_values.each { |value| data.push_word value }
100
+ data
101
+ end
102
+
103
+
104
+ # Returns the length of the register values in bytes.
105
+ #
106
+ # @return [Integer] The length.
107
+ #
108
+ def byte_count
109
+ @reg_values.size * 2
110
+ end
111
+
112
+
113
+ # Returns the length of the PDU in bytes.
114
+ #
115
+ # @return [Integer] The length.
116
+ #
117
+ def length
118
+ # +1 for func_code, +1 for byte_count
119
+ byte_count + 2
120
+ end
121
+
122
+
123
+ # Validates the PDU. Raises exceptions if validation fails.
124
+ #
125
+ def validate
126
+
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+
133
+ end # Modbus
134
+
135
+
@@ -0,0 +1,158 @@
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 WriteMultipleRegistersRequest < PDU
12
+ FUNC_CODE = 0x10
13
+
14
+ attr_accessor :start_addr, :reg_values
15
+
16
+
17
+ # Initializes a new PDU instance. Decodes from protocol data if given.
18
+ #
19
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
20
+ #
21
+ def initialize(data = nil, func_code = nil)
22
+ @start_addr = 0
23
+ @reg_values = []
24
+ super
25
+ end
26
+
27
+
28
+ # Decodes a PDU from protocol data.
29
+ #
30
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
31
+ #
32
+ def decode(data)
33
+ @start_addr = data.shift_word
34
+
35
+ reg_count = data.shift_word
36
+ fail ClientError, "Register count must be in (1..127), got #{reg_count}" unless (1..127).include?(reg_count)
37
+
38
+ byte_count = data.shift_byte
39
+ fail ClientError, "Byte count does not match available data, expected #{byte_count} bytes, got #{data.size} bytes." unless byte_count == data.size
40
+
41
+ reg_count.times { @reg_values.push data.shift_word }
42
+ end
43
+
44
+
45
+ # Encodes a PDU into protocol data.
46
+ #
47
+ # @return [Modbus::ProtocolData] The protocol data representation of this object.
48
+ #
49
+ def encode
50
+ data = super
51
+ data.push_word @start_addr
52
+ data.push_word reg_count
53
+ data.push_byte byte_count
54
+ @reg_values.each { |value| data.push_word value }
55
+ data
56
+ end
57
+
58
+
59
+ # Returns the length of the register values in bytes.
60
+ #
61
+ # @return [Integer] The length.
62
+ #
63
+ def byte_count
64
+ reg_count * 2
65
+ end
66
+
67
+
68
+ # Returns the number of registers to write.
69
+ #
70
+ # @return [Integer] The number of registers.
71
+ #
72
+ def reg_count
73
+ @reg_values.size
74
+ end
75
+
76
+
77
+ # Returns the length of the PDU in bytes.
78
+ #
79
+ # @return [Integer] The length.
80
+ #
81
+ def length
82
+ # +1 for func_code, +2 for starting address, +2 for reg_count, +1 for byte_count
83
+ byte_count + 6
84
+ end
85
+
86
+
87
+ # Validates the PDU. Raises exceptions if validation fails.
88
+ #
89
+ def validate
90
+
91
+ end
92
+
93
+ end
94
+
95
+
96
+ # PDU for modbus function "read holding register" (response message)
97
+ #
98
+ class WriteMultipleRegistersResponse < PDU
99
+ FUNC_CODE = 0x10
100
+
101
+ attr_accessor :start_addr, :reg_count
102
+
103
+
104
+ # Initializes a new PDU instance. Decodes from protocol data if given.
105
+ #
106
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
107
+ #
108
+ def initialize(data = nil, func_code = nil)
109
+ @start_addr = 0
110
+ @reg_count = 0
111
+ super
112
+ end
113
+
114
+
115
+ # Decodes a PDU from protocol data.
116
+ #
117
+ # @param data [Modbus::ProtocolData] The protocol data to decode.
118
+ #
119
+ def decode(data)
120
+ @start_addr = data.shift_word
121
+ @reg_count = data.shift_word
122
+ end
123
+
124
+
125
+ # Encodes a PDU into protocol data.
126
+ #
127
+ # @return [Modbus::ProtocolData] The protocol data representation of this object.
128
+ #
129
+ def encode
130
+ data = super
131
+ data.push_word @start_addr
132
+ data.push_word @reg_count
133
+ data
134
+ end
135
+
136
+
137
+ # Returns the length of the PDU in bytes.
138
+ #
139
+ # @return [Integer] The length.
140
+ #
141
+ def length
142
+ 5
143
+ end
144
+
145
+
146
+ # Validates the PDU. Raises exceptions if validation fails.
147
+ #
148
+ def validate
149
+ fail ClientError, "Register count must be in (1..127), got '#{@reg_count.inspect}'" unless (1..127).include?(@reg_count)
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+
156
+ end # Modbus
157
+
158
+
@@ -0,0 +1,19 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+
6
+ module Modbus
7
+
8
+ class Register
9
+ attr_reader :addr
10
+ attr_accessor :handler
11
+
12
+
13
+ def initialize(addr)
14
+ @addr = addr
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,49 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+
6
+ module Modbus
7
+
8
+ class BitRegister < Register
9
+
10
+ def initialize(addr)
11
+ super
12
+ @bits = {}
13
+ end
14
+
15
+
16
+ def update(bit, value)
17
+ fail ArgumentError unless (0..15).include? bit
18
+ fail ArgumentError unless [true, false].include? value
19
+ @bits[bit] = value
20
+ end
21
+
22
+
23
+ def value
24
+ result = 0
25
+
26
+ @bits.each do |bit, bit_value|
27
+ value = bit_value ? 1 : 0
28
+ result |= (value << bit)
29
+ end
30
+
31
+ result
32
+ end
33
+
34
+
35
+ def write(value)
36
+ return unless @handler
37
+
38
+ values = @bits.keys.map do |bit|
39
+ bit_value = value[bit] == 1
40
+ {:addr => [@addr, bit].join('.'), :value => bit_value}
41
+ end
42
+
43
+ @handler.write_values values
44
+ end
45
+
46
+ end
47
+
48
+
49
+ end
@@ -0,0 +1,7 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+ require 'modbus/register/base'
5
+ require 'modbus/register/bit_register'
6
+ require 'modbus/register/word_register'
7
+
@@ -0,0 +1,30 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+
6
+ module Modbus
7
+
8
+ class WordRegister < Register
9
+ attr_reader :value
10
+
11
+
12
+ def initialize(addr)
13
+ super
14
+ @value = 0
15
+ end
16
+
17
+
18
+ def update(value)
19
+ fail ArgumentError unless (0..65535).include? value
20
+ @value = value
21
+ end
22
+
23
+
24
+ def write(value)
25
+ @handler.write_values [{:addr => @addr.to_s, :value => value}] if @handler
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,120 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+ require 'uri'
5
+
6
+ module Modbus
7
+
8
+ class Server
9
+ attr_reader :registers
10
+
11
+
12
+ def initialize(uri, handler)
13
+ @uri = URI uri
14
+ @handler = handler
15
+ @registers = {}
16
+ end
17
+
18
+
19
+ def add_register(addr, handler = nil)
20
+ log.info "Adding register @ #{addr}"
21
+
22
+ reg_addr, bit = addr.split('.')
23
+ reg_addr = reg_addr.to_i
24
+
25
+ register_class = bit ? BitRegister : WordRegister
26
+ reg = get_register reg_addr, register_class
27
+ reg.handler = handler
28
+
29
+ value = bit ? false : 0
30
+ update_register addr, value
31
+
32
+ reg
33
+ end
34
+
35
+
36
+ def update_register(addr, value)
37
+ reg_addr, bit = addr.split('.')
38
+ reg = @registers.fetch reg_addr.to_i
39
+
40
+ case reg
41
+ when WordRegister
42
+ reg.update value
43
+ when BitRegister
44
+ reg.update bit.to_i, value
45
+ end
46
+ end
47
+
48
+
49
+ def start
50
+ EM.start_server @uri.host, @uri.port, Modbus::Connection::TCPServer, self
51
+ end
52
+
53
+
54
+ def client_connected(signature)
55
+ log.info "client connected (signature #{signature})"
56
+ end
57
+
58
+
59
+ def client_disconnected(signature)
60
+ log.info "client disconnected (signature #{signature})"
61
+ end
62
+
63
+
64
+ def read_registers(start_addr, reg_count)
65
+ (0..reg_count-1).map do |idx|
66
+ addr = start_addr + idx
67
+ read_register addr
68
+ end
69
+ end
70
+
71
+
72
+ def write_registers(start_addr, reg_values)
73
+ reg_values.each_with_index do |value, idx|
74
+ addr = start_addr + idx
75
+ write_register addr, value
76
+ end
77
+
78
+ reg_values.size
79
+ end
80
+
81
+
82
+ private
83
+
84
+
85
+ def log
86
+ @handler.log
87
+ end
88
+
89
+
90
+ def get_register(addr, klass)
91
+ @registers[addr] ||= klass.new(addr)
92
+ end
93
+
94
+
95
+ def read_register(addr)
96
+ reg = @registers.fetch addr
97
+ reg.value
98
+
99
+ rescue IndexError
100
+ log.warn "read_register @ #{addr} failed (IllegalDataAddress)"
101
+ fail IllegalDataAddress
102
+ rescue => e
103
+ log.warn "read_register @ #{addr} failed. Error: #{e.message} (#{e.class}), Line: #{e.backtrace.first}"
104
+ end
105
+
106
+
107
+ def write_register(addr, value)
108
+ reg = @registers.fetch addr
109
+ reg.write value
110
+
111
+ rescue IndexError
112
+ log.warn "write_register @ #{addr} failed (IllegalDataAddress)"
113
+ fail IllegalDataAddress
114
+ rescue => e
115
+ log.warn "write_register @ #{addr} failed. Error: #{e.message}, Line: #{e.backtrace.first}"
116
+ end
117
+
118
+ end
119
+
120
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright © 2016 Andy Rohr <andy.rohr@mindclue.ch>
2
+ # All rights reserved.
3
+
4
+
5
+ module Modbus
6
+ module Transaction
7
+
8
+ class Base
9
+
10
+
11
+ # Initializes a new Transaction instance.
12
+ #
13
+ # @param conn [Modbus::Connection::TCPServer] An EM connection object to work on.
14
+ #
15
+ def initialize(conn)
16
+ @conn = conn
17
+ end
18
+
19
+ end # Base
20
+
21
+ end # Transaction
22
+
23
+ end # Modbus
24
+
25
+