modbus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5559848be6158b9fdf9c0d2a8d030b08769f181510cfba3a59f90a7a8af9e723
4
+ data.tar.gz: aade6afbf693c033ccb3bd4b021592d0dc09ceff7247ce4a9f66de3491e0bba2
5
+ SHA512:
6
+ metadata.gz: 5b2ff3ee1a9dfcc6ce804879ec11158db23c1581ce4e9134d439fbc1035e7d8b150cd65a291d91dac3a6a2e150459768dcf83a10967f6469eb0c07a3fd55c11a
7
+ data.tar.gz: f2409419ec8923a69dccc82e6d340138e320126befa6f7f9add418419a24aa376ce1147f0b33d4f8f3e7451e905b9ea226b1a21f655628b1c0d8703f97510d5b
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Ruby Modbus
2
+
3
+ Constructs [modbus standard](http://www.modbus.org/specs.php) datagrams that make it easy to communicate with devices on modbus networks.
4
+ It does not implement the transport layer so you can use it with native ruby, eventmachine, celluloid or the like.
5
+
6
+ [![Build Status](https://travis-ci.org/acaprojects/ruby-modbus.svg?branch=master)](https://travis-ci.org/acaprojects/ruby-modbus)
7
+
8
+ You'll need a gateway that supports TCP/IP.
9
+
10
+
11
+ ## Install the gem
12
+
13
+ Install it with [RubyGems](https://rubygems.org/)
14
+
15
+ gem install modbus
16
+
17
+ or add this to your Gemfile if you use [Bundler](http://gembundler.com/):
18
+
19
+ gem 'modbus'
20
+
21
+
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ require 'modbus'
27
+
28
+ modbus = Modbus.new
29
+
30
+ # Reading input obtained from the network
31
+ # The class instance performs buffering and yields complete ADUs
32
+ modbus.read(byte_string) do |adu|
33
+ adu.header.transaction_identifier # => 32
34
+
35
+ # Response PDU returned
36
+ if adu.exception?
37
+ puts adu.pdu.exception_code
38
+ else
39
+ case adu.function_code
40
+ when 0x01
41
+ puts adu.pdu.read.data.bytes
42
+ end
43
+ end
44
+ end
45
+
46
+
47
+ # You can generate requests like so (writing multipe coils starting from 123)
48
+ request = modbus.write_coils(123, true, true, false)
49
+ byte_string = request.to_binary_s
50
+
51
+ # Read 4 coils starting from address 123
52
+ request = modbus.read_coils(123, 4)
53
+ byte_string = request.to_binary_s
54
+
55
+ # Send byte_string to the modbus gateway to execute the request
56
+
57
+ ```
58
+
59
+
60
+ ## License and copyright
61
+
62
+ MIT
data/lib/modbus.rb ADDED
@@ -0,0 +1,191 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ require 'bindata'
5
+
6
+ require 'modbus/tcp_header'
7
+ require 'modbus/request'
8
+ require 'modbus/response'
9
+ require 'modbus/exception'
10
+ require 'modbus/adu'
11
+
12
+ class Modbus
13
+ CODES = {
14
+ read_coils: 0x01,
15
+ read_inputs: 0x02,
16
+ read_holding_registers: 0x03,
17
+ read_input_registers: 0x04,
18
+ write_coil: 0x05,
19
+ write_register: 0x06,
20
+ write_multiple_coils: 0x0F,
21
+ write_multiple_registers: 0x10,
22
+
23
+ # Serial only
24
+ read_exception_status: 0x07,
25
+ diagnostics: 0x08,
26
+ get_event_counter: 0x0B,
27
+ get_event_log: 0x0C,
28
+ get_server_id: 0x11,
29
+
30
+ # unsupported
31
+ read_file_record: 0x14,
32
+ write_file_record: 0x15,
33
+ mask_write_register: 0x16,
34
+ read_write_registers: 0x17,
35
+ read_fifo_queue: 0x17,
36
+ encapsulated_interface_transport: 0x2B,
37
+ canopen_general_request: 0x0D,
38
+ read_device_identification: 0x0E
39
+ }
40
+ CODES.merge!(CODES.invert)
41
+ CODES.freeze
42
+
43
+ def initialize
44
+ @transaction = 0
45
+ end
46
+
47
+ # Decodes an ADU from wire format and sets the attributes of this object.
48
+ #
49
+ # @param data [String] The bytes to decode.
50
+ def read(data)
51
+ @buffer ||= String.new
52
+ @buffer << data
53
+
54
+ error = nil
55
+
56
+ loop do
57
+ # not enough data in buffer to know the length
58
+ break if @buffer.bytesize < 6
59
+
60
+ header = TCPHeader.new
61
+ header.read(@buffer[0..5])
62
+
63
+ # the headers unit identifier is included in the length
64
+ total_length = header.request_length + 6
65
+
66
+ # Extract just the request from the buffer
67
+ break if @buffer.bytesize < total_length
68
+ request = @buffer.slice!(0...total_length)
69
+ function_code = request.getbyte(7)
70
+
71
+ # Yield the complete responses
72
+ begin
73
+ if function_code <= 0x80
74
+ response = ResponsePDU.new
75
+ response.read(request[6..-1])
76
+ else # Error response
77
+ response = ExceptionPDU.new
78
+ response.read(request[6..-1])
79
+ function_code = function_code - 0x80
80
+ end
81
+
82
+ yield ADU.new header, function_code, response
83
+ rescue => e
84
+ error = e
85
+ end
86
+ end
87
+
88
+ raise error if error
89
+ end
90
+
91
+ [:read_coils, :read_inputs, :read_holding_registers, :read_input_registers].each do |function|
92
+ define_method function do |address, count = 1|
93
+ adu = request_adu function
94
+ request = adu.pdu
95
+ request.get.address = address.to_i
96
+ request.get.quantity = count.to_i
97
+ adu
98
+ end
99
+ end
100
+
101
+ def write_coils(address, *values)
102
+ values = values.flatten
103
+
104
+ if values.length > 1
105
+ write_multiple_coils(address, *values)
106
+ else
107
+ adu = request_adu :write_coil
108
+ request = adu.pdu
109
+ request.put.address = address.to_i
110
+ request.put.data = values.first ? 0xFF00 : 0x0
111
+ adu
112
+ end
113
+ end
114
+
115
+ def write_registers(address, *values)
116
+ values = values.flatten.map! { |value| value.to_i }
117
+
118
+ if values.length > 1
119
+ adu = request_adu :write_multiple_registers
120
+ request = adu.pdu
121
+ request.put_multiple.address = address.to_i
122
+ request.put_multiple.quantity = values.length
123
+ request.put_multiple.data = values.pack('n*')
124
+ adu
125
+ else
126
+ adu = request_adu :write_register
127
+ request = adu.pdu
128
+ request.put.address = address.to_i
129
+ request.put.data = values.first
130
+ adu
131
+ end
132
+ end
133
+
134
+ protected
135
+
136
+ def write_multiple_coils(address, *values)
137
+ size = values.length
138
+
139
+ bytes = []
140
+ byte = 0
141
+ bit = 0
142
+ loop do
143
+ value = values.shift
144
+ if value
145
+ byte = byte | (1 << bit)
146
+ end
147
+ break if values.empty?
148
+ bit += 1
149
+ if bit >= 8
150
+ bytes << byte
151
+ byte = 0
152
+ bit = 0
153
+ end
154
+ end
155
+ bytes << byte
156
+
157
+ adu = request_adu :write_multiple_coils
158
+ request = adu.pdu
159
+ request.put_multiple.address = address.to_i
160
+ request.put_multiple.quantity = size
161
+ request.put_multiple.data = bytes.pack('C*')
162
+ adu
163
+ end
164
+
165
+ def next_id
166
+ id = @transaction
167
+ @transaction += 1
168
+ @transaction = 0 if @transaction > 0xFFFF
169
+ id
170
+ end
171
+
172
+ def new_header
173
+ header = TCPHeader.new
174
+ header.transaction_identifier = next_id
175
+ header.protocol_identifier = 0
176
+ header
177
+ end
178
+
179
+ def request_pdu(code)
180
+ request = RequestPDU.new
181
+ # This seems to be the default
182
+ request.unit_identifier = 0xFF
183
+ request.function_code = code
184
+ request
185
+ end
186
+
187
+ def request_adu(function)
188
+ code = CODES[function]
189
+ ADU.new(new_header, code, request_pdu(code))
190
+ end
191
+ end
data/lib/modbus/adu.rb ADDED
@@ -0,0 +1,48 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ class Modbus
5
+ # TCP ADU are sent via TCP to registered port 502
6
+ Modbus::ADU = Struct.new :header, :function_code, :pdu do
7
+ def exception?
8
+ pdu.is_a?(ExceptionPDU)
9
+ end
10
+
11
+ def to_binary_s
12
+ data = pdu.to_binary_s
13
+ header.request_length = data.bytesize
14
+ "#{header.to_binary_s}#{data}"
15
+ end
16
+
17
+ def function_name
18
+ CODES[function_code]
19
+ end
20
+
21
+ def value
22
+ return EXCEPTIONS[pdu.exception_code] || "unknown error 0x#{pdu.exception_code.to_s(16)}" if exception?
23
+ return nil unless pdu.is_a?(ResponsePDU) && READ_CODES.include?(function_code)
24
+
25
+ bytes = pdu.get.data_length
26
+ bin_str = pdu.get.data
27
+
28
+ case function_name
29
+ when :read_coils, :read_inputs
30
+ values = []
31
+
32
+ # extract the bits and return an array of true / false values
33
+ bin_str.each_byte do |byte|
34
+ bit = 0
35
+ loop do
36
+ values << ((byte & (1 << bit)) > 0)
37
+ bit += 1
38
+ break if bit >= 8
39
+ end
40
+ end
41
+ values
42
+ when :read_holding_registers, :read_input_registers
43
+ # these are all 16bit integers
44
+ bin_str.unpack('n*')
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ class Modbus
5
+ EXCEPTIONS = {
6
+ 0x01 => 'illegal function',
7
+ 0x02 => 'illegal data address',
8
+ 0x03 => 'illegal data value',
9
+ 0x04 => 'server device failure',
10
+ 0x05 => 'acknowledge', # processing will take some time, no need to retry
11
+ 0x06 => 'server device busy',
12
+ 0x08 => 'memory parity error',
13
+ 0x0A => 'gateway path unavailable',
14
+ 0x0B => 'gateway device failed to respond'
15
+ }
16
+
17
+ class ExceptionPDU < BinData::Record
18
+ endian :big
19
+
20
+ uint8 :unit_identifier
21
+ uint8 :function_code
22
+ uint8 :exception_code
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ class Modbus
5
+ READ_CODES = [0x01, 0x02, 0x03, 0x04].freeze
6
+ WRITE_CODES = [0x05, 0x06].freeze
7
+ MULTIPLE_CODES = [0x0F, 0x10].freeze
8
+
9
+ class RequestPDU < BinData::Record
10
+ endian :big
11
+
12
+ # This field is used for intra-system routing purpose (default to 0xFF)
13
+ uint8 :unit_identifier
14
+ uint8 :function_code
15
+
16
+ struct :get, onlyif: -> { READ_CODES.include? function_code } do
17
+ uint16 :address
18
+ uint16 :quantity
19
+ end
20
+
21
+ struct :put, onlyif: -> { WRITE_CODES.include? function_code } do
22
+ uint16 :address
23
+ uint16 :data
24
+ end
25
+
26
+ struct :put_multiple, onlyif: -> { MULTIPLE_CODES.include? function_code } do
27
+ uint16 :address
28
+ uint16 :quantity
29
+
30
+ uint8 :data_length, value: -> { put_multiple.data.bytesize }
31
+ string :data, read_length: -> { put_multiple.data_length }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ class Modbus
5
+ class ResponsePDU < BinData::Record
6
+ endian :big
7
+
8
+ # This field is used for intra-system routing purpose (default to 0xFF)
9
+ uint8 :unit_identifier
10
+ uint8 :function_code
11
+
12
+ struct :get, onlyif: -> { READ_CODES.include? function_code } do
13
+ uint8 :data_length, value: -> { get.data.bytesize }
14
+ string :data, read_length: -> { get.data_length }
15
+ end
16
+
17
+ struct :put, onlyif: -> { WRITE_CODES.include?(function_code) || MULTIPLE_CODES.include?(function_code) } do
18
+ uint16 :address
19
+ uint16 :data
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ class Modbus
5
+ # TCP ADU are sent via TCP to registered port 502
6
+ class TCPHeader < BinData::Record
7
+ endian :big
8
+
9
+ # used for transaction pairing
10
+ uint16 :transaction_identifier
11
+
12
+ # The MODBUS protocol is identified by the value 0.
13
+ uint16 :protocol_identifier
14
+
15
+ # byte count of the following fields, including the Unit Identifier and data fields.
16
+ uint16 :request_length
17
+ end
18
+ end
data/modbus.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "modbus"
5
+ s.version = '1.0.0'
6
+ s.authors = ["Stephen von Takach"]
7
+ s.email = ["steve@aca.im"]
8
+ s.licenses = ["MIT"]
9
+ s.homepage = "https://github.com/acaprojects/ruby-modbus"
10
+ s.summary = "Modbus protocol on Ruby"
11
+ s.description = <<-EOF
12
+ Constructs Modbus standard datagrams that make it easy to communicate with devices on Modbus networks
13
+ EOF
14
+
15
+
16
+ s.add_dependency 'bindata', '~> 2.3'
17
+
18
+ s.add_development_dependency 'rspec', '~> 3.5'
19
+ s.add_development_dependency 'rake', '~> 12'
20
+
21
+
22
+ s.files = Dir["{lib}/**/*"] + %w(modbus.gemspec README.md)
23
+ s.test_files = Dir["spec/**/*"]
24
+ s.extra_rdoc_files = ["README.md"]
25
+
26
+ s.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,73 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ require 'modbus'
5
+
6
+ # Example captures
7
+ # http://www.pcapr.net/browse?q=modbus
8
+
9
+ describe 'modbus protocol helper' do
10
+ before :each do
11
+ @modbus = Modbus.new
12
+ @response = nil
13
+ end
14
+
15
+ it "should parse and generate the same string" do
16
+ data = "\x0\x0\x0\x0\x0\x4\x1\x1\x1\x1"
17
+ @modbus.read(data) { |adu| @response = adu }
18
+ expect(@response.to_binary_s).to eq(data)
19
+ @response = nil
20
+
21
+ data = "\x0\x0\x0\x0\x0\x4\x1\x2\x1\x1"
22
+ @modbus.read(data) { |adu| @response = adu }
23
+ expect(@response.to_binary_s).to eq(data)
24
+ @response = nil
25
+ end
26
+
27
+ it "should generate a read coil request" do
28
+ data = @modbus.read_coils(0).to_binary_s
29
+ expect(data).to eq("\x0\x0\x0\x0\x0\x6\xFF\x1\x0\x0\x0\x1")
30
+ end
31
+
32
+ it "should generate a read inputs request" do
33
+ data = @modbus.read_inputs(0).to_binary_s
34
+ expect(data).to eq("\x0\x0\x0\x0\x0\x6\xFF\x2\x0\x0\x0\x1")
35
+ end
36
+
37
+ it "should generate a write coils request" do
38
+ data = @modbus.write_coils(4, true).to_binary_s
39
+ expect(data).to eq("\x00\x00\x00\x00\x00\x06\xFF\x05\x00\x04\xFF\x00")
40
+
41
+ data = @modbus.write_coils(3, false).to_binary_s
42
+ expect(data).to eq("\x00\x01\x00\x00\x00\x06\xFF\x05\x00\x03\x00\x00")
43
+
44
+ data = @modbus.write_coils(4, true, true).to_binary_s
45
+ expect(data).to eq("\x00\x02\x00\x00\x00\x08\xFF\x0F\x00\x04\x00\x02\x1\x3")
46
+
47
+ data = @modbus.write_coils(4, false, true).to_binary_s
48
+ expect(data).to eq("\x00\x03\x00\x00\x00\x08\xFF\x0F\x00\x04\x00\x02\x1\x2")
49
+
50
+ data = @modbus.write_coils(4, true, true, true, true, true, true, true, true, false, true).to_binary_s
51
+ expect(data).to eq("\x00\x04\x00\x00\x00\x09\xFF\x0F\x00\x04\x00\x0A\x2\xFF\x2")
52
+ end
53
+
54
+ it "should generate a write registers request" do
55
+ data = @modbus.write_registers(5, [1, 2, 3, 4]).to_binary_s
56
+ expect(data).to eq("\x00\x00\x00\x00\x00\x0F\xFF\x10\x00\x05\x00\x04\x8\x00\x01\x00\x02\x00\x03\x00\x04")
57
+
58
+ data = @modbus.write_registers(5, 4).to_binary_s
59
+ expect(data).to eq("\x00\x01\x00\x00\x00\x06\xFF\x06\x00\x05\x00\x04")
60
+ end
61
+
62
+ it "should return the response values" do
63
+ data = "\x00\x00\x00\x00\x00\x04\xFF\x01\x1\x3"
64
+ @modbus.read(data) { |adu| @response = adu }
65
+ expect(@response.value).to eq([true, true, false, false, false, false, false, false])
66
+ expect(@response.function_name).to eq(:read_coils)
67
+
68
+ data = "\x00\x00\x00\x00\x00\x07\xFF\x03\x4\x0\x3\x1\x00"
69
+ @modbus.read(data) { |adu| @response = adu }
70
+ expect(@response.value).to eq([3, 256])
71
+ expect(@response.function_name).to eq(:read_holding_registers)
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: modbus
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen von Takach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-08-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bindata
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12'
55
+ description: " Constructs Modbus standard datagrams that make it easy to communicate
56
+ with devices on Modbus networks\n"
57
+ email:
58
+ - steve@aca.im
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files:
62
+ - README.md
63
+ files:
64
+ - README.md
65
+ - lib/modbus.rb
66
+ - lib/modbus/adu.rb
67
+ - lib/modbus/exception.rb
68
+ - lib/modbus/request.rb
69
+ - lib/modbus/response.rb
70
+ - lib/modbus/tcp_header.rb
71
+ - modbus.gemspec
72
+ - spec/modbus_spec.rb
73
+ homepage: https://github.com/acaprojects/ruby-modbus
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 2.7.7
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Modbus protocol on Ruby
97
+ test_files:
98
+ - spec/modbus_spec.rb