modbus-cli 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .*swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in modbus-cli.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,212 @@
1
+ modbus-cli
2
+ ==========
3
+
4
+ modbus-cli is a command line utility that lets you read and write data using
5
+ the Modbus TCP protocol (ethernet only, no serial line). It supports different
6
+ data formats (bool, int, word, float, dword), allows you to save data to a file
7
+ and dump it back to your device, acting as a backup tool, or allowing you to
8
+ move blocks in memory.
9
+
10
+ [Home page]:http://www.github.com/tallakt/momdbus-cli
11
+
12
+ Installation
13
+ ------------
14
+
15
+ Install ruby. Then install the gem:
16
+
17
+ $ gem install modbus-cli
18
+
19
+ The pure ruby gem should run on most rubies.
20
+
21
+ Quick Start
22
+ -----------
23
+
24
+ Lets start by reading five words from our device startig from address %MW100.
25
+
26
+ $ modbus read 192.168.0.1 %MW100 5
27
+
28
+ which writes
29
+
30
+ %MW100 0
31
+ %MW101 0
32
+ %MW102 0
33
+ %MW103 0
34
+ %MW104 0
35
+
36
+ We chose to write the address in Schneider format, %MW100, but you can also use Modicon naming convention.
37
+ The following achieves the same as the previous line
38
+
39
+ $ modbus read 192.168.0.1 400101 5
40
+
41
+ To read coils run the command
42
+
43
+ $ modbus read 192.168.0.1 %M100 5
44
+
45
+ or
46
+
47
+ $ modbus read 192.168.0.1 101 5
48
+
49
+ You get three subcommands, read, write and dump. The dump commands writes data previously read
50
+ using the read command back to its original location. You can get more info on the commands by
51
+ using the help parameter
52
+
53
+ $ modbus read --help
54
+
55
+ To write data, write the values after the offset
56
+
57
+ $ modbus write 192.168.0.1 101 1 2 3 4 5
58
+
59
+ Please be aware that there is no protection here - you could easily mess up a running production
60
+ system by doing this.
61
+
62
+ Data Types
63
+ ----------
64
+
65
+ For Schneider format you can choose between different data types by using different addresses,
66
+ When using Modicon addresses, you may specify the data type with an additional parameter.
67
+ The supported data types are shown in the following table
68
+
69
+ <table>
70
+ <tr>
71
+ <th>Data type</th>
72
+ <th>Data size</th>
73
+ <th>Schneider address</th>
74
+ <th>Modicon address</th>
75
+ <th>Parameter</th>
76
+ </tr>
77
+ <tr>
78
+ <td>word (default, unsigned)</td>
79
+ <td>16 bits</td>
80
+ <td>%MW100</td>
81
+ <td>400101</td>
82
+ <td>--word</td>
83
+ </tr>
84
+ <tr>
85
+ <td>integer (signed)</td>
86
+ <td>16 bits</td>
87
+ <td>%MW100</td>
88
+ <td>400101</td>
89
+ <td>--int</td>
90
+ </tr>
91
+ <tr>
92
+ <td>floating point</td>
93
+ <td>32 bits</td>
94
+ <td>%MF100</td>
95
+ <td>400101</td>
96
+ <td>--float</td>
97
+ </tr>
98
+ <tr>
99
+ <td>double word</td>
100
+ <td>32 bits</td>
101
+ <td>%MD100</td>
102
+ <td>400101</td>
103
+ <td>--dword</td>
104
+ </tr>
105
+ <tr>
106
+ <td>boolean (coils)</td>
107
+ <td>1 bit</td>
108
+ <td>%M100</td>
109
+ <td>101</td>
110
+ <td>N/A</td>
111
+ </tr>
112
+ </table>
113
+
114
+ To read a floating point value, issue the following command
115
+
116
+ $ modbus read %MF100 2
117
+
118
+ which should give you something like
119
+
120
+ %MF100 0.0
121
+ %MF102 0.0
122
+
123
+ or alternatively
124
+
125
+ $ modbus read --float 400101 2
126
+
127
+ giving
128
+
129
+ 400101 0.0
130
+ 400103 0.0
131
+
132
+ The modbus command supports the addressing areas 1..99999 for coils and 400001..499999 for the rest using Modicon addresses. Using Schneider addresses the %M addresses are in a separate memory from %MW values, but %MW, %MD, %MF all reside in a shared memory, so %MW0 and %MW1 share the memory with %MF0.
133
+
134
+
135
+ Reading and dumping to files
136
+ ----------------------------
137
+
138
+ The following functionality has a few potential uses:
139
+
140
+ * Storing a backup of PLC memory containing setpoints and such in event og hardware failure
141
+ * Moving a block from one location in the PLC to another location
142
+ * Copy data from one machine to another
143
+
144
+ First, start by reading data from your device to be stored in a file
145
+
146
+ $ modbus read --output mybackup.yml 192.168.0.1 400001 1000
147
+
148
+ on Linux you may want to look at the text file by doing
149
+
150
+ $ less mybackup.yml
151
+
152
+ on Windows try loading the file in Wordpad.
153
+
154
+ To restore the memory at a later time, run the command (again a word of warning, this can mess
155
+ up a running production system)
156
+
157
+ $ modbus dump mybackup.yml
158
+
159
+ The modbus command supports multiple files, so feel free to write
160
+
161
+ $ modbus dump *.yml
162
+
163
+ To write the data back to a different device, use the --host parameter
164
+
165
+ $ modbus dump --host 192.168.0.2 mybackup.yml
166
+
167
+ or for a different memory location
168
+
169
+ $ modbus dump --offset 401001 192.168.0.2 mybackup.yml
170
+
171
+ or for a different slave id
172
+
173
+ $ modbus dump --slave 88 192.168.0.2 mybackup.yml
174
+
175
+ Slave ids are not commonly necessary when working with Modbus TCP.
176
+
177
+ Contributing to modbus-cli
178
+ --------------------------
179
+
180
+ Feel free to fork the project on GitHub and send fork requests. Please
181
+ try to have each feature separated in commits.
182
+
183
+
184
+
185
+ License
186
+ -------
187
+
188
+ (The MIT License)
189
+
190
+ Copyright (C) 2011 Tallak Tveide
191
+
192
+ Permission is hereby granted, free of charge, to any person obtaining a copy
193
+ of this software and associated documentation files (the "Software"), to
194
+ deal in the Software without restriction, including without limitation the
195
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
196
+ sell copies of the Software, and to permit persons to whom the Software is
197
+ furnished to do so, subject to the following conditions:
198
+
199
+ The above copyright notice and this permission notice shall be included in
200
+ all copies or substantial portions of the Software.
201
+
202
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
203
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
204
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
205
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
206
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
207
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
208
+
209
+
210
+
211
+
212
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
data/bin/modbus ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ require 'modbus-cli'
5
+
6
+ Modbus::Cli::CommandLineRunner.run
7
+
data/lib/modbus-cli.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'clamp'
2
+
3
+ # do it this way to not have serialport warning on startup
4
+ # require 'rmodbus'
5
+ require 'rmodbus/errors'
6
+ require 'rmodbus/ext'
7
+ require 'rmodbus/debug'
8
+ require 'rmodbus/options'
9
+ require 'rmodbus/rtu'
10
+ require 'rmodbus/tcp'
11
+ require 'rmodbus/slave'
12
+ require 'rmodbus/client'
13
+ require 'rmodbus/server'
14
+ require 'rmodbus/tcp_slave'
15
+ require 'rmodbus/tcp_client'
16
+ require 'rmodbus/tcp_server'
17
+
18
+ require 'modbus-cli/version'
19
+ require 'modbus-cli/read_command'
20
+ require 'modbus-cli/write_command'
21
+ require 'modbus-cli/dump_command'
22
+
23
+ module Modbus
24
+ module Cli
25
+ DEFAULT_SLAVE = 1
26
+
27
+ class CommandLineRunner < Clamp::Command
28
+ subcommand 'read', 'read from the device', ReadCommand
29
+ subcommand 'write', 'write to the device', WriteCommand
30
+ subcommand 'dump', 'copy contents of read file to the device', DumpCommand
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,130 @@
1
+ module Modbus
2
+ module Cli
3
+ module CommandsCommon
4
+
5
+ MAX_WRITE_COILS = 1968
6
+ MAX_WRITE_WORDS = 123
7
+ DEFAULT_SLAVE = 1
8
+
9
+ module ClassMethods
10
+
11
+ def host_parameter
12
+ parameter 'HOST', 'IP address or hostname for the Modbus device', :attribute_name => :host
13
+ end
14
+
15
+
16
+ def address_parameter
17
+ parameter 'ADDRESS', 'Start address, in Schneider format (eg %M100, %MW100)', :attribute_name => :address do |a|
18
+ schneider_match(a) || modicon_match(a) || raise(ArgumentError, "Illegal address #{a}")
19
+ end
20
+ end
21
+
22
+ def datatype_options
23
+ option ["-w", "--word"], :flag, "use signed 16 bit integers"
24
+ option ["-i", "--int"], :flag, "use signed 16 bit integers"
25
+ option ["-d", "--dword"], :flag, "use signed 16 bit integers"
26
+ option ["-f", "--float"], :flag, "use signed 16 bit integers"
27
+ end
28
+
29
+ def format_options
30
+ option ["--modicon"], :flag, "use Modicon addressing (eg. coil: 101, word: 400001)"
31
+ option ["--schneider"], :flag, "use Schneider addressing (eg. coil: %M100, word: %MW0, float: %MF0, dword: %MD0)"
32
+ end
33
+
34
+ def slave_option
35
+ option ["-s", "--slave"], 'ID', "use slave id ID", :default => 1 do |s|
36
+ Integer(s).tap {|slave| raise ArgumentError 'Slave must be in the range 0..255' unless (0..255).member?(slave) }
37
+ end
38
+ end
39
+
40
+ def output_option
41
+ end
42
+ end
43
+
44
+
45
+ def data_size
46
+ case addr_type
47
+ when :bit, :word, :int
48
+ 1
49
+ when :float, :dword
50
+ 2
51
+ end
52
+ end
53
+
54
+
55
+ def addr_offset
56
+ address[:offset]
57
+ end
58
+
59
+ def addr_type
60
+ if int?
61
+ :int
62
+ elsif dword?
63
+ :dword
64
+ elsif float?
65
+ :float
66
+ elsif word?
67
+ :word
68
+ else
69
+ address[:datatype]
70
+ end
71
+ end
72
+
73
+
74
+ def schneider_match(address)
75
+ schneider_match = address.match /%M([FWD])?(\d+)/i
76
+ if schneider_match
77
+ {:offset => schneider_match[2].to_i, :format => :schneider}.tap do |result|
78
+ case schneider_match[1]
79
+ when nil
80
+ result[:datatype] = :bit
81
+ when 'W', 'w'
82
+ result[:datatype] = :word
83
+ when 'F', 'f'
84
+ result[:datatype] = :float
85
+ when 'D', 'd'
86
+ result[:datatype] = :dword
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+
93
+ def modicon_match(address)
94
+ if address.match /^\d+$/
95
+ offset = address.to_i
96
+ case offset
97
+ when 1..99999
98
+ {:offset => offset - 1, :datatype => :bit, :format => :modicon}
99
+ when 400001..499999
100
+ {:offset => offset - 400001, :datatype => :word, :format => :modicon}
101
+ end
102
+ end
103
+ end
104
+
105
+ def addr_format
106
+ if schneider?
107
+ :schneider
108
+ elsif modicon?
109
+ :modicon
110
+ else
111
+ address[:format]
112
+ end
113
+ end
114
+
115
+ def sliced_write_registers(sl, offset, data)
116
+ (0..(data.count - 1)).each_slice(MAX_WRITE_WORDS) do |slice|
117
+ result = sl.write_holding_registers(slice.first + offset, data.values_at(*slice))
118
+ end
119
+ end
120
+
121
+ def sliced_write_coils(sl, offset, data)
122
+ (0..(data.count - 1)).each_slice(MAX_WRITE_COILS) do |slice|
123
+ result = sl.write_multiple_coils(slice.first + offset, data.values_at(*slice))
124
+ end
125
+ end
126
+
127
+ end
128
+ end
129
+ end
130
+
@@ -0,0 +1,64 @@
1
+ require 'modbus-cli/commands_common'
2
+ require 'yaml'
3
+
4
+ module Modbus
5
+ module Cli
6
+ class DumpCommand < Clamp::Command
7
+ extend CommandsCommon::ClassMethods
8
+ include CommandsCommon
9
+
10
+ parameter 'FILES ...', 'restore data in FILES to original devices (created by modbus read command)', :attribute_name => :files do |f|
11
+ f.map do |filename|
12
+ YAML.load_file(filename).dup.tap do |ff|
13
+ #parameter takes presedence
14
+ ff[:host] = host || ff[:host]
15
+ ff[:slave] = slave || ff[:slave]
16
+ ff[:offset] = offset || ff[:offset]
17
+ end
18
+ end
19
+ end
20
+
21
+ option ["-h", "--host"], 'ADDR', "use the address/hostname ADDR instead of the stored one"
22
+
23
+ option ["-s", "--slave"], 'ID', "use slave ID instead of the stored one" do |s|
24
+ Integer(s).tap {|slave| raise ArgumentError 'Slave address should be in the range 0..255' unless (0..255).member? slave }
25
+ end
26
+
27
+ option ["-o", "--offset"], 'OFFSET', "start writing at address OFFSET instead of original location" do |o|
28
+ raise ArgumentError 'Illegal offset address: ' + o unless modicon_match(o) || schneider_match(o)
29
+ o
30
+ end
31
+
32
+ def execute
33
+ host_ids = files.map {|d| d[:host] }.sort.uniq
34
+ host_ids.each {|host_id| execute_host host_id }
35
+ end
36
+
37
+ def execute_host(host_id)
38
+ slave_ids = files.select {|d| d[:host] == host_id }.map {|d| d[:slave] }.sort.uniq
39
+ ModBus::TCPClient.connect(host_id) do |client|
40
+ slave_ids.each {|slave_id| execute_slave host_id, slave_id, client }
41
+ end
42
+ end
43
+
44
+ def execute_slave(host_id, slave_id, client)
45
+ client.with_slave(slave_id) do |slave|
46
+ files.select {|d| d[:host] == host_id && d[:slave] == slave_id }.each do |file_data|
47
+ execute_single_file slave, file_data
48
+ end
49
+ end
50
+ end
51
+
52
+ def execute_single_file(slave, file_data)
53
+ address = modicon_match(file_data[:offset].to_s) || schneider_match(file_data[:offset].to_s)
54
+ case address[:datatype]
55
+ when :bit
56
+ sliced_write_coils slave, address[:offset], file_data[:data]
57
+ when :word
58
+ sliced_write_registers slave, address[:offset], file_data[:data]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,160 @@
1
+ require 'modbus-cli/commands_common'
2
+ require 'yaml'
3
+
4
+ module Modbus
5
+ module Cli
6
+ class ReadCommand < Clamp::Command
7
+ extend CommandsCommon::ClassMethods
8
+ include CommandsCommon
9
+
10
+ MAX_READ_COIL_COUNT = 2000
11
+ MAX_READ_WORD_COUNT = 125
12
+
13
+ datatype_options
14
+ format_options
15
+ slave_option
16
+ host_parameter
17
+ address_parameter
18
+ option ["-o", "--output"], 'FILE', "write results to file FILE"
19
+
20
+ parameter 'COUNT', 'number of data to read', :attribute_name => :count do |c|
21
+ result = Integer(c)
22
+ raise ArgumentError, 'Count must be positive' if result <= 0
23
+ result
24
+
25
+ end
26
+
27
+ def read_floats(sl)
28
+ floats = read_and_unpack(sl, 'g')
29
+ (0...count).each do |n|
30
+ puts "#{ '%-10s' % address_to_s(addr_offset + n * data_size)} #{nice_float('% 16.8f' % floats[n])}"
31
+ end
32
+ end
33
+
34
+ def read_dwords(sl)
35
+ dwords = read_and_unpack(sl, 'N')
36
+ (0...count).each do |n|
37
+ puts "#{ '%-10s' % address_to_s(addr_offset + n * data_size)} #{'%10d' % dwords[n]}"
38
+ end
39
+ end
40
+
41
+ def read_registers(sl, options = {})
42
+ data = read_data_words(sl)
43
+ if options[:int]
44
+ data = data.pack('S').unpack('s')
45
+ end
46
+ read_range.zip(data).each do |pair|
47
+ puts "#{ '%-10s' % address_to_s(pair.first)} #{'%6d' % pair.last}"
48
+ end
49
+ end
50
+
51
+ def read_words_to_file(sl)
52
+ write_data_to_file(read_data_words(sl))
53
+ end
54
+
55
+ def read_coils_to_file(sl)
56
+ write_data_to_file(read_data_coils(sl))
57
+ end
58
+
59
+ def write_data_to_file(data)
60
+ File.open(output, 'w') do |file|
61
+ file.puts({ :host => host, :slave => slave, :offset => address_to_s(addr_offset, :modicon), :data => data }.to_yaml)
62
+ end
63
+ end
64
+
65
+ def read_coils(sl)
66
+ data = read_data_coils(sl)
67
+ read_range.zip(data) do |pair|
68
+ puts "#{ '%-10s' % address_to_s(pair.first)} #{'%d' % pair.last}"
69
+ end
70
+ end
71
+
72
+ def execute
73
+ ModBus::TCPClient.connect(host) do |cl|
74
+ cl.with_slave(slave) do |sl|
75
+ if output then
76
+ case addr_type
77
+ when :bit
78
+ read_coils_to_file(sl)
79
+ else
80
+ read_words_to_file(sl)
81
+ end
82
+ else
83
+ case addr_type
84
+ when :bit
85
+ read_coils(sl)
86
+ when :int
87
+ read_registers(sl, :int => true)
88
+ when :word
89
+ read_registers(sl)
90
+ when :float
91
+ read_floats(sl)
92
+ when :dword
93
+ read_dwords(sl)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def read_and_unpack(sl, format)
101
+ # the word ordering is wrong. calling reverse two times effectively swaps every pair
102
+ read_data_words(sl).reverse.pack('n*').unpack("#{format}*").reverse
103
+ end
104
+
105
+ def read_data_words(sl)
106
+ result = []
107
+ read_range.each_slice(MAX_READ_WORD_COUNT) {|slice| result += sl.read_holding_registers(slice.first, slice.count) }
108
+ result
109
+ end
110
+
111
+
112
+ def read_data_coils(sl)
113
+ result = []
114
+ read_range.each_slice(MAX_READ_COIL_COUNT) do |slice|
115
+ result += sl.read_coils(slice.first, slice.count)
116
+ end
117
+ result
118
+ end
119
+
120
+ def read_range
121
+ (addr_offset..(addr_offset + count * data_size - 1))
122
+ end
123
+
124
+
125
+ def nice_float(str)
126
+ m = str.match /(.*[.][0-9])0*/
127
+ if m
128
+ m[1]
129
+ else
130
+ str
131
+ end
132
+ end
133
+
134
+ def address_to_s(addr, format = addr_format)
135
+ case format
136
+ when :schneider
137
+ case addr_type
138
+ when :bit
139
+ '%M' + addr.to_s
140
+ when :word, :int
141
+ '%MW' + addr.to_s
142
+ when :dword
143
+ '%MD' + addr.to_s
144
+ when :float
145
+ '%MF' + addr.to_s
146
+ end
147
+ when :modicon
148
+ case addr_type
149
+ when :bit
150
+ (addr + 1).to_s
151
+ when :word, :int
152
+ (addr + 400001).to_s
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+
@@ -0,0 +1,5 @@
1
+ module Modbus
2
+ module Cli
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,81 @@
1
+ require 'modbus-cli/commands_common'
2
+
3
+ module Modbus
4
+ module Cli
5
+ class WriteCommand < Clamp::Command
6
+ extend CommandsCommon::ClassMethods
7
+ include CommandsCommon
8
+
9
+
10
+ datatype_options
11
+ format_options
12
+ slave_option
13
+ host_parameter
14
+ address_parameter
15
+
16
+ parameter 'VALUES ...', 'values to write, nonzero counts as true for discrete values', :attribute_name => :values do |vv|
17
+ case addr_type
18
+
19
+ when :bit
20
+ int_parameter vv, 0, 1
21
+ when :word
22
+ int_parameter vv, 0, 0xffff
23
+ when :int
24
+ int_parameter vv, -32768, 32767
25
+ when :dword
26
+ int_parameter vv, 0, 0xffffffff
27
+ when :float
28
+ vv.map {|v| Float(v) }
29
+ end
30
+ end
31
+
32
+
33
+
34
+ def execute
35
+ ModBus::TCPClient.connect(host) do |cl|
36
+ cl.with_slave(slave) do |sl|
37
+ case addr_type
38
+ when :bit
39
+ write_coils sl
40
+ when :word, :int
41
+ write_words sl
42
+ when :float
43
+ write_floats sl
44
+ when :dword
45
+ write_dwords sl
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def write_coils(sl)
52
+ sliced_write_coils sl, addr_offset, values
53
+ end
54
+
55
+ def write_words(sl)
56
+ sliced_write_registers sl, addr_offset, values.pack('S*').unpack('S*')
57
+ end
58
+
59
+ def write_floats(sl)
60
+ pack_and_write sl, 'g'
61
+ end
62
+
63
+ def write_dwords(sl)
64
+ pack_and_write sl, 'N'
65
+ end
66
+
67
+ def pack_and_write(sl, format)
68
+ # the word ordering is wrong. calling reverse two times effectively swaps every pair
69
+ sliced_write_registers(sl, addr_offset, values.reverse.pack("#{format}*").unpack('n*').reverse)
70
+ end
71
+
72
+ def int_parameter(vv, min, max)
73
+ vv.map {|v| Integer(v) }.tap do |values|
74
+ values.each do |v|
75
+ raise ArgumentError, "Value should be in the range #{min}..#{max}" unless (min..max).member? v
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "modbus-cli/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "modbus-cli"
7
+ s.version = Modbus::Cli::VERSION
8
+ s.authors = ["Tallak Tveide"]
9
+ s.email = ["tallak@tveide.net"]
10
+ s.homepage = "http://www.github.com/tallakt/modbus-cli"
11
+ s.summary = %q{Modbus command line}
12
+ s.description = %q{Command line interface to communicate over Modbus TCP}
13
+
14
+ s.rubyforge_project = "modbus-cli"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ s.add_runtime_dependency "rmodbus", '= 1.1.0'
24
+ s.add_runtime_dependency "clamp", '= 0.3.0'
25
+ s.add_development_dependency "rspec", '= 2.7.0'
26
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+
4
+
5
+ describe Modbus::Cli::DumpCommand do
6
+ before(:each) do
7
+ stub_tcpip
8
+ end
9
+
10
+ it 'reads the file and write the contents to the original device' do
11
+ client = mock 'client'
12
+ slave = mock 'slave'
13
+ YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :slave => 5, :offset => 400123, :data => [4, 5, 6])
14
+ ModBus::TCPClient.should_receive(:connect).with('1.2.3.4').and_yield(client)
15
+ client.should_receive(:with_slave).with(5).and_yield(slave)
16
+ slave.should_receive(:write_holding_registers).with(122, [4, 5, 6])
17
+ cmd.run %w(dump dump.yml)
18
+ end
19
+
20
+ it 'can read two files from separate hosts' do
21
+ client1 = mock 'client1'
22
+ client2 = mock 'client2'
23
+ slave1 = mock 'slave1'
24
+ slave2 = mock 'slave2'
25
+ yml = {:host => 'X', :slave => 5, :offset => 400010, :data => [99]}
26
+ YAML.should_receive(:load_file).with('a.yml').and_return(yml)
27
+ YAML.should_receive(:load_file).with('b.yml').and_return(yml.dup.tap {|y| y[:host] = 'Y' })
28
+ ModBus::TCPClient.should_receive(:connect).with('X').and_yield(client1)
29
+ ModBus::TCPClient.should_receive(:connect).with('Y').and_yield(client2)
30
+ client1.should_receive(:with_slave).with(5).and_yield(slave1)
31
+ client2.should_receive(:with_slave).with(5).and_yield(slave2)
32
+ slave1.should_receive(:write_holding_registers).with(9, [99])
33
+ slave2.should_receive(:write_holding_registers).with(9, [99])
34
+ cmd.run %w(dump a.yml b.yml)
35
+ end
36
+
37
+ it 'can dump two files from separate slaves on same host' do
38
+ client1 = mock 'client1'
39
+ slave1 = mock 'slave1'
40
+ slave2 = mock 'slave2'
41
+ yml = {:host => 'X', :slave => 5, :offset => 400010, :data => [99]}
42
+ YAML.should_receive(:load_file).with('a.yml').and_return(yml)
43
+ YAML.should_receive(:load_file).with('b.yml').and_return(yml.dup.tap {|y| y[:slave] = 99 })
44
+ ModBus::TCPClient.should_receive(:connect).with('X').and_yield(client1)
45
+ client1.should_receive(:with_slave).with(5).and_yield(slave1)
46
+ client1.should_receive(:with_slave).with(99).and_yield(slave2)
47
+ slave1.should_receive(:write_holding_registers).with(9, [99])
48
+ slave2.should_receive(:write_holding_registers).with(9, [99])
49
+ cmd.run %w(dump a.yml b.yml)
50
+ end
51
+
52
+ it 'can dump two files from one slave' do
53
+ client1 = mock 'client1'
54
+ slave1 = mock 'slave1'
55
+ yml = {:host => 'X', :slave => 5, :offset => 400010, :data => [99]}
56
+ YAML.should_receive(:load_file).with('a.yml').and_return(yml)
57
+ YAML.should_receive(:load_file).with('b.yml').and_return(yml.dup)
58
+ ModBus::TCPClient.should_receive(:connect).with('X').and_yield(client1)
59
+ client1.should_receive(:with_slave).with(5).and_yield(slave1)
60
+ slave1.should_receive(:write_holding_registers).with(9, [99])
61
+ slave1.should_receive(:write_holding_registers).with(9, [99])
62
+ cmd.run %w(dump a.yml b.yml)
63
+ end
64
+
65
+ it 'accepts the --host <hostname> parameter' do
66
+ YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :slave => 5, :offset => 123, :data => [4, 5, 6])
67
+ ModBus::TCPClient.should_receive(:connect).with('Y')
68
+ cmd.run %w(dump --host Y dump.yml)
69
+ end
70
+
71
+ it 'accepts the --slave <id> parameter' do
72
+ client = mock 'client'
73
+ YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :slave => 5, :offset => 123, :data => [4, 5, 6])
74
+ ModBus::TCPClient.should_receive(:connect).with('1.2.3.4').and_yield(client)
75
+ client.should_receive(:with_slave).with(99)
76
+ cmd.run %w(dump --slave 99 dump.yml)
77
+ end
78
+
79
+ it 'accepts the --offset <n> parameter with modicon addressing' do
80
+ client = mock 'client'
81
+ slave = mock 'slave'
82
+ YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :slave => 5, :offset => 123, :data => [4, 5, 6])
83
+ ModBus::TCPClient.should_receive(:connect).with('1.2.3.4').and_yield(client)
84
+ client.should_receive(:with_slave).with(5).and_yield(slave)
85
+ slave.should_receive(:write_holding_registers).with(100, [4, 5, 6])
86
+ cmd.run %w(dump --offset 400101 dump.yml)
87
+ end
88
+
89
+ it 'accepts the --offset <n> parameter with schneider addressing' do
90
+ client = mock 'client'
91
+ slave = mock 'slave'
92
+ YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :slave => 5, :offset => 123, :data => [4, 5, 6])
93
+ ModBus::TCPClient.should_receive(:connect).with('1.2.3.4').and_yield(client)
94
+ client.should_receive(:with_slave).with(5).and_yield(slave)
95
+ slave.should_receive(:write_holding_registers).with(100, [4, 5, 6])
96
+ cmd.run %w(dump --offset %MW100 dump.yml)
97
+ end
98
+ end
99
+
100
+
101
+
102
+
103
+
104
+
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+
4
+
5
+ describe Modbus::Cli::CommandLineRunner do
6
+ include OutputCapture
7
+
8
+ before(:each) do
9
+ stub_tcpip
10
+ end
11
+
12
+ it 'has help describing the read and write commands' do
13
+ c = cmd
14
+ Proc.new { c.run(%w(--help)) }.should raise_exception(Clamp::HelpWanted)
15
+ c.help.should match /usage:/i
16
+ c.help.should match /read/
17
+ c.help.should match /write/
18
+ c.help.should match /dump/
19
+ end
20
+ end
21
+
22
+
23
+
24
+
@@ -0,0 +1,156 @@
1
+ require 'spec_helper'
2
+ require 'yaml'
3
+
4
+
5
+
6
+ describe Modbus::Cli::ReadCommand do
7
+ include OutputCapture
8
+
9
+ before(:each) do
10
+ stub_tcpip
11
+ end
12
+
13
+ it 'can read registers' do
14
+ client, slave = standard_connect_helper '1.2.3.4'
15
+ slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
16
+ cmd.run %w(read 1.2.3.4 %MW100 10)
17
+ stdout.should match(/^\s*%MW105\s*5$/)
18
+ end
19
+
20
+
21
+ it 'can read floating point numbers' do
22
+ client, slave = standard_connect_helper '1.2.3.4'
23
+ slave.should_receive(:read_holding_registers).with(100, 4).and_return([52429, 17095, 52429, 17095])
24
+ cmd.run %w(read 1.2.3.4 %MF100 2)
25
+ stdout.should match(/^\s*%MF102\s*99[.]9(00[0-9]*)?$/)
26
+ end
27
+
28
+ it 'can read double word numbers' do
29
+ client, slave = standard_connect_helper '1.2.3.4'
30
+ slave.should_receive(:read_holding_registers).with(100, 4).and_return([16959, 15, 16959, 15])
31
+ cmd.run %w(read 1.2.3.4 %MD100 2)
32
+ stdout.should match(/^\s*%MD102\s*999999$/)
33
+ end
34
+
35
+ it 'can read coils' do
36
+ client, slave = standard_connect_helper '1.2.3.4'
37
+ slave.should_receive(:read_coils).with(100, 10).and_return([1, 0] * 5)
38
+ cmd.run %w(read 1.2.3.4 %M100 10)
39
+ stdout.should match(/^\s*%M105\s*0$/)
40
+ end
41
+
42
+
43
+
44
+
45
+ it 'rejects illegal counts' do
46
+ lambda { cmd.run %w(read 1.2.3.4 %MW100 1+0) }.should raise_exception(Clamp::UsageError)
47
+ lambda { cmd.run %w(read 1.2.3.4 %MW100 -10) }.should raise_exception(Clamp::UsageError)
48
+ lambda { cmd.run %w(read 1.2.3.4 %MW100 9.9) }.should raise_exception(Clamp::UsageError)
49
+ end
50
+
51
+ it 'rejects illegal addresses' do
52
+ lambda { cmd.run %w(read 1.2.3.4 %MW1+00) }.should raise_exception(Clamp::UsageError)
53
+ end
54
+
55
+ it 'should split large reads into smaller chunks for words' do
56
+ client, slave = standard_connect_helper '1.2.3.4'
57
+ slave.should_receive(:read_holding_registers).with(100, 125).and_return([1, 0] * 1000)
58
+ slave.should_receive(:read_holding_registers).with(225, 25).and_return([1, 0] * 1000)
59
+ cmd.run %w(read 1.2.3.4 %MW100 150)
60
+ end
61
+
62
+ it 'should split large reads into smaller chunks for coils' do
63
+ client, slave = standard_connect_helper '1.2.3.4'
64
+ slave.should_receive(:read_coils).with(100, 2000).and_return([1, 0] * 1000)
65
+ slave.should_receive(:read_coils).with(2100, 1000).and_return([1, 0] * 500)
66
+ cmd.run %w(read 1.2.3.4 %M100 3000)
67
+ end
68
+
69
+ it 'can read registers as ints' do
70
+ client, slave = standard_connect_helper '1.2.3.4'
71
+ slave.should_receive(:read_holding_registers).with(100, 1).and_return([0xffff])
72
+ cmd.run %w(read --int 1.2.3.4 %MW100 1)
73
+ stdout.should match(/^\s*%MW100\s*-1$/)
74
+ end
75
+
76
+ it 'can read registers as floats' do
77
+ client, slave = standard_connect_helper '1.2.3.4'
78
+ slave.should_receive(:read_holding_registers).with(100, 2).and_return([0,0])
79
+ cmd.run %w(read --float 1.2.3.4 %MW100 1)
80
+ end
81
+
82
+ it 'can read registers as dwords' do
83
+ client, slave = standard_connect_helper '1.2.3.4'
84
+ slave.should_receive(:read_holding_registers).with(100, 2).and_return([0,0])
85
+ cmd.run %w(read --dword 1.2.3.4 %MW100 1)
86
+ end
87
+
88
+ it 'can read registers as words' do
89
+ client, slave = standard_connect_helper '1.2.3.4'
90
+ slave.should_receive(:read_holding_registers).with(100, 1).and_return([0])
91
+ cmd.run %w(read --word 1.2.3.4 %MD100 1)
92
+ end
93
+
94
+ it 'accepts Modicon addresses for coils' do
95
+ client, slave = standard_connect_helper '1.2.3.4'
96
+ slave.should_receive(:read_coils).with(100, 10).and_return([1, 0] * 5)
97
+ cmd.run %w(read 1.2.3.4 101 10)
98
+ stdout.should match(/^\s*106\s*0$/)
99
+ end
100
+
101
+ it 'accepts Modicon addresses for registers' do
102
+ client, slave = standard_connect_helper '1.2.3.4'
103
+ slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
104
+ cmd.run %w(read 1.2.3.4 400101 10)
105
+ stdout.should match(/^\s*400106\s*5$/)
106
+ end
107
+
108
+
109
+ it 'should accept the --modicon option to force modicon output' do
110
+ client, slave = standard_connect_helper '1.2.3.4'
111
+ slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
112
+ cmd.run %w(read --modicon 1.2.3.4 %MW100 10)
113
+ stdout.should match(/^\s*400106\s*5$/)
114
+ end
115
+
116
+ it 'should accept the --schneider option to force schneider output' do
117
+ client, slave = standard_connect_helper '1.2.3.4'
118
+ slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
119
+ cmd.run %w(read --schneider 1.2.3.4 400101 10)
120
+ stdout.should match(/^\s*%MW105\s*5$/)
121
+ end
122
+
123
+ it 'has a --slave parameter' do
124
+ client = mock 'client'
125
+ ModBus::TCPClient.should_receive(:connect).with('X').and_yield(client)
126
+ client.should_receive(:with_slave).with(99)
127
+ cmd.run %w(read --slave 99 X 101 1)
128
+ end
129
+
130
+ it 'can write the output from reading registers to a yaml file using the -o <filename> parameter' do
131
+ client, slave = standard_connect_helper '1.2.3.4'
132
+ slave.should_receive(:read_holding_registers).with(100, 1).and_return([1])
133
+ file_mock = mock('file')
134
+ File.should_receive(:open).and_yield(file_mock)
135
+ file_mock.should_receive(:puts).with({:host => '1.2.3.4', :slave => 1, :offset => '400101', :data => [1]}.to_yaml)
136
+ cmd.run %w(read --output filename.yml 1.2.3.4 %MW100 1)
137
+ stdout.should_not match(/./)
138
+ end
139
+
140
+ it 'can write the output from reading coils to a yaml file using the -o <filename> parameter' do
141
+ client, slave = standard_connect_helper '1.2.3.4'
142
+ slave.should_receive(:read_coils).with(100, 1).and_return([1])
143
+ file_mock = mock('file')
144
+ File.should_receive(:open).and_yield(file_mock)
145
+ file_mock.should_receive(:puts).with({:host => '1.2.3.4', :slave => 1, :offset => '101', :data => [1]}.to_yaml)
146
+ cmd.run %w(read --output filename.yml 1.2.3.4 %M100 1)
147
+ stdout.should_not match(/./)
148
+ end
149
+
150
+
151
+ end
152
+
153
+
154
+
155
+
156
+
@@ -0,0 +1,48 @@
1
+ require 'rspec'
2
+ require 'clamp'
3
+ require 'stringio'
4
+ require 'modbus-cli'
5
+
6
+ # Borrowed from Clamp tests
7
+ module OutputCapture
8
+
9
+ def self.included(target)
10
+ target.before do
11
+ $stdout = @out = StringIO.new
12
+ $stderr = @err = StringIO.new
13
+ end
14
+ target.after do
15
+ $stdout = STDOUT
16
+ $stderr = STDERR
17
+ end
18
+ end
19
+
20
+ def stdout
21
+ @out.string
22
+ end
23
+
24
+ def stderr
25
+ @err.string
26
+ end
27
+
28
+
29
+
30
+ end
31
+
32
+ def stub_tcpip
33
+ TCPSocket.stub!(:new) # prevent comms with actual PLC or device
34
+ end
35
+
36
+
37
+ def standard_connect_helper(address)
38
+ client = mock 'client'
39
+ slave = mock 'slave'
40
+ ModBus::TCPClient.should_receive(:connect).with(address).and_yield(client)
41
+ client.should_receive(:with_slave).with(1).and_yield(slave)
42
+ return client, slave
43
+ end
44
+
45
+
46
+ def cmd
47
+ Modbus::Cli::CommandLineRunner.new('modbus-cli')
48
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+
4
+
5
+ describe Modbus::Cli::WriteCommand do
6
+ include OutputCapture
7
+
8
+ before(:each) do
9
+ stub_tcpip
10
+ end
11
+
12
+ it 'can write to registers' do
13
+ client, slave = standard_connect_helper 'HOST'
14
+ slave.should_receive(:write_holding_registers).with(100, [1, 2, 3, 4])
15
+ cmd.run %w(write HOST %MW100 1 2 3 4)
16
+ end
17
+
18
+
19
+ it 'can write floating point numbers' do
20
+ client, slave = standard_connect_helper 'HOST'
21
+ slave.should_receive(:write_holding_registers).with(100, [52429, 17095, 52429, 17095])
22
+ cmd.run %w(write HOST %MF100 99.9 99.9)
23
+ end
24
+
25
+ it 'can write double word numbers' do
26
+ client, slave = standard_connect_helper 'HOST'
27
+ slave.should_receive(:write_holding_registers).with(100, [16959, 15, 16959, 15])
28
+ cmd.run %w(write HOST %MD100 999999 999999)
29
+ end
30
+
31
+ it 'can write to coils' do
32
+ client, slave = standard_connect_helper 'HOST'
33
+ slave.should_receive(:write_multiple_coils).with(100, [1, 0, 1, 0, 1, 0, 0, 1, 1])
34
+ cmd.run %w(write HOST %M100 1 0 1 0 1 0 0 1 1)
35
+ end
36
+
37
+ it 'rejects illegal values' do
38
+ lambda { cmd.run %w(write 1.2.3.4 %MW100 10 tust) }.should raise_exception(Clamp::UsageError)
39
+ lambda { cmd.run %w(write 1.2.3.4 %MW100 9999999) }.should raise_exception(Clamp::UsageError)
40
+ end
41
+
42
+ it 'rejects illegal addresses' do
43
+ lambda { cmd.run %w(write 1.2.3.4 %MW1+00 ) }.should raise_exception(Clamp::UsageError)
44
+ end
45
+
46
+
47
+ it 'should split large writes in chunks for words' do
48
+ client, slave = standard_connect_helper 'HOST'
49
+ slave.should_receive(:write_holding_registers).with(100, (1..123).to_a)
50
+ slave.should_receive(:write_holding_registers).with(223, (124..150).to_a)
51
+ cmd.run %w(write HOST %MW100) + (1..150).to_a
52
+ end
53
+
54
+ it 'should split large writes in chunks for coils' do
55
+ client, slave = standard_connect_helper 'HOST'
56
+ slave.should_receive(:write_multiple_coils).with(100, [0, 1] * 984)
57
+ slave.should_receive(:write_multiple_coils).with(2068, [0, 1] * 16)
58
+ cmd.run %w(write HOST %M100) + [0, 1] * 1000
59
+ end
60
+
61
+ it 'can write to registers as ints' do
62
+ client, slave = standard_connect_helper 'HOST'
63
+ slave.should_receive(:write_holding_registers).with(100, [0xffff])
64
+ cmd.run %w(write --int HOST %MW100 -1)
65
+ end
66
+
67
+ it 'can write to registers as floats' do
68
+ client, slave = standard_connect_helper 'HOST'
69
+ slave.should_receive(:write_holding_registers).with(100, [52429, 17095])
70
+ cmd.run %w(write --float HOST %MW100 99.9)
71
+ end
72
+
73
+ it 'can write to registers as double words' do
74
+ client, slave = standard_connect_helper 'HOST'
75
+ slave.should_receive(:write_holding_registers).with(100, [16959, 15])
76
+ cmd.run %w(write --dword HOST %MW100 999999)
77
+ end
78
+
79
+ it 'can write to registers as words' do
80
+ client, slave = standard_connect_helper 'HOST'
81
+ slave.should_receive(:write_holding_registers).with(100, [99])
82
+ cmd.run %w(write --word HOST %MF100 99)
83
+ end
84
+
85
+ it 'can write to registers using Modicon addressing' do
86
+ client, slave = standard_connect_helper 'HOST'
87
+ slave.should_receive(:write_holding_registers).with(100, [1, 2, 3, 4])
88
+ cmd.run %w(write HOST 400101 1 2 3 4)
89
+ end
90
+
91
+ it 'can write to coils using Modicon addressing' do
92
+ client, slave = standard_connect_helper 'HOST'
93
+ slave.should_receive(:write_multiple_coils).with(100, [1, 0, 1, 0, 1, 0, 0, 1, 1])
94
+ cmd.run %w(write HOST 101 1 0 1 0 1 0 0 1 1)
95
+ end
96
+
97
+ it 'has a --slave parameter' do
98
+ client = mock 'client'
99
+ ModBus::TCPClient.should_receive(:connect).with('X').and_yield(client)
100
+ client.should_receive(:with_slave).with(99)
101
+ cmd.run %w(write --slave 99 X 101 1)
102
+ end
103
+
104
+ end
105
+
106
+
107
+
108
+
109
+
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: modbus-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tallak Tveide
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-30 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rmodbus
16
+ requirement: &76170020 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - =
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *76170020
25
+ - !ruby/object:Gem::Dependency
26
+ name: clamp
27
+ requirement: &76169740 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - =
31
+ - !ruby/object:Gem::Version
32
+ version: 0.3.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *76169740
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &76169500 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - =
42
+ - !ruby/object:Gem::Version
43
+ version: 2.7.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *76169500
47
+ description: Command line interface to communicate over Modbus TCP
48
+ email:
49
+ - tallak@tveide.net
50
+ executables:
51
+ - modbus
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - README.markdown
58
+ - Rakefile
59
+ - bin/modbus
60
+ - lib/modbus-cli.rb
61
+ - lib/modbus-cli/commands_common.rb
62
+ - lib/modbus-cli/dump_command.rb
63
+ - lib/modbus-cli/read_command.rb
64
+ - lib/modbus-cli/version.rb
65
+ - lib/modbus-cli/write_command.rb
66
+ - modbus-cli.gemspec
67
+ - spec/dump_command_spec.rb
68
+ - spec/modbus_cli_spec.rb
69
+ - spec/read_command_spec.rb
70
+ - spec/spec_helper.rb
71
+ - spec/write_command_spec.rb
72
+ homepage: http://www.github.com/tallakt/modbus-cli
73
+ licenses: []
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project: modbus-cli
92
+ rubygems_version: 1.8.10
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Modbus command line
96
+ test_files: []
97
+ has_rdoc: