├── .gitignore
├── .travis.yml
├── Gemfile
├── README.markdown
├── Rakefile
├── bin
└── modbus
├── lib
├── modbus-cli.rb
└── modbus-cli
│ ├── commands_common.rb
│ ├── dump_command.rb
│ ├── read_command.rb
│ ├── version.rb
│ └── write_command.rb
├── modbus-cli.gemspec
└── spec
├── dump_command_spec.rb
├── modbus_cli_spec.rb
├── read_command_spec.rb
├── spec_helper.rb
└── write_command_spec.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 | .*swp
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | rvm:
2 | - 2.5.1
3 | - 2.1.0
4 | - 1.9.3
5 | - jruby
6 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in modbus-cli.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
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 | [](https://travis-ci.org/tallakt/modbus-cli)
13 |
14 | Installation
15 | ------------
16 |
17 | Install ruby. Then install the gem:
18 |
19 | $ gem install modbus-cli
20 |
21 | The pure ruby gem should run on most rubies.
22 |
23 | Quick Start
24 | -----------
25 |
26 | Let's start by reading five words from our device starting from address %MW100.
27 |
28 | $ modbus read 192.168.0.1 %MW100 5
29 |
30 | which writes
31 |
32 | %MW100 0
33 | %MW101 0
34 | %MW102 0
35 | %MW103 0
36 | %MW104 0
37 |
38 | We chose to write the address in Schneider format, %MW100, but you can also use Modicon naming convention.
39 | The following achieves the same as the previous line
40 |
41 | $ modbus read 192.168.0.1 400101 5
42 |
43 | To read coils run the command
44 |
45 | $ modbus read 192.168.0.1 %M100 5
46 |
47 | or
48 |
49 | $ modbus read 192.168.0.1 101 5
50 |
51 | You get three subcommands, read, write and dump. The dump commands writes data previously read
52 | using the read command back to its original location. You can get more info on the commands by
53 | using the help parameter
54 |
55 | $ modbus read --help
56 |
57 | To write data, write the values after the offset
58 |
59 | $ modbus write 192.168.0.1 101 1 2 3 4 5
60 |
61 | Please be aware that there is no protection here - you could easily mess up a running production
62 | system by doing this.
63 |
64 | Data Types
65 | ----------
66 |
67 | For Schneider format you can choose between different data types by using different addresses,
68 | When using Modicon addresses, you may specify the data type with an additional parameter.
69 | The supported data types are shown in the following table
70 |
71 |
72 |
73 | Data type |
74 | Data size |
75 | Schneider address |
76 | Modicon address |
77 | Parameter |
78 |
79 |
80 | word (default, unsigned) |
81 | 16 bits |
82 | %MW100 |
83 | 400101 |
84 | --word |
85 |
86 |
87 | integer (signed) |
88 | 16 bits |
89 | %MW100 |
90 | 400101 |
91 | --int |
92 |
93 |
94 | floating point |
95 | 32 bits |
96 | %MF100 |
97 | 400101 |
98 | --float |
99 |
100 |
101 | double word |
102 | 32 bits |
103 | %MD100 |
104 | 400101 |
105 | --dword |
106 |
107 |
108 | boolean (coils) |
109 | 1 bit |
110 | %M100 |
111 | 101 |
112 | N/A |
113 |
114 |
115 |
116 | To read a floating point value, issue the following command
117 |
118 | $ modbus read %MF100 2
119 |
120 | which should give you something like
121 |
122 | %MF100 0.0
123 | %MF102 0.0
124 |
125 | or alternatively
126 |
127 | $ modbus read --float 400101 2
128 |
129 | giving
130 |
131 | 400101 0.0
132 | 400103 0.0
133 |
134 | 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.
135 |
136 |
137 | Reading and dumping to files
138 | ----------------------------
139 |
140 | The following functionality has a few potential uses:
141 |
142 | * Storing a backup of PLC memory containing setpoints and such in event og hardware failure
143 | * Moving a block from one location in the PLC to another location
144 | * Copy data from one machine to another
145 |
146 | First, start by reading data from your device to be stored in a file
147 |
148 | $ modbus read --output mybackup.yml 192.168.0.1 400001 1000
149 |
150 | on Linux you may want to look at the text file by doing
151 |
152 | $ less mybackup.yml
153 |
154 | on Windows try loading the file in Wordpad.
155 |
156 | To restore the memory at a later time, run the command (again a word of warning, this can mess
157 | up a running production system)
158 |
159 | $ modbus dump mybackup.yml
160 |
161 | The modbus command supports multiple files, so feel free to write
162 |
163 | $ modbus dump *.yml
164 |
165 | To write the data back to a different device, use the --host parameter
166 |
167 | $ modbus dump --host 192.168.0.2 mybackup.yml
168 |
169 | or for a different memory location
170 |
171 | $ modbus dump --offset 401001 192.168.0.2 mybackup.yml
172 |
173 | or for a different slave id
174 |
175 | $ modbus dump --slave 88 192.168.0.2 mybackup.yml
176 |
177 | Slave ids are not commonly necessary when working with Modbus TCP.
178 |
179 | Contributing to modbus-cli
180 | --------------------------
181 |
182 | Feel free to fork the project on GitHub and send fork requests. Please
183 | try to have each feature separated in commits.
184 |
185 |
186 |
187 | License
188 | -------
189 |
190 | (The MIT License)
191 |
192 | Copyright (C) 2011-2015 Tallak Tveide
193 |
194 | Permission is hereby granted, free of charge, to any person obtaining a copy
195 | of this software and associated documentation files (the "Software"), to
196 | deal in the Software without restriction, including without limitation the
197 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
198 | sell copies of the Software, and to permit persons to whom the Software is
199 | furnished to do so, subject to the following conditions:
200 |
201 | The above copyright notice and this permission notice shall be included in
202 | all copies or substantial portions of the Software.
203 |
204 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
205 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
206 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
207 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
208 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
209 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
210 |
211 |
212 |
213 |
214 |
215 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rspec/core/rake_task'
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task :default => :spec
7 |
8 |
--------------------------------------------------------------------------------
/bin/modbus:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 |
4 | require 'modbus-cli'
5 |
6 | Modbus::Cli::CommandLineRunner.run
7 |
8 |
--------------------------------------------------------------------------------
/lib/modbus-cli.rb:
--------------------------------------------------------------------------------
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/client'
12 | require 'rmodbus/server'
13 | require 'rmodbus/tcp_slave'
14 | require 'rmodbus/tcp_client'
15 | require 'rmodbus/tcp_server'
16 |
17 | require 'modbus-cli/version'
18 | require 'modbus-cli/read_command'
19 | require 'modbus-cli/write_command'
20 | require 'modbus-cli/dump_command'
21 |
22 | module Modbus
23 | module Cli
24 | DEFAULT_SLAVE = 1
25 |
26 | class CommandLineRunner < Clamp::Command
27 | subcommand 'read', 'read from the device', ReadCommand
28 | subcommand 'write', 'write to the device', WriteCommand
29 | subcommand 'dump', 'copy contents of read file to the device', DumpCommand
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/modbus-cli/commands_common.rb:
--------------------------------------------------------------------------------
1 | module Modbus
2 | module Cli
3 | module CommandsCommon
4 |
5 | MAX_WRITE_COILS = 800
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 (eg %M100, %MW100, 101, 400101)', :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 unsigned 16 bit integers"
24 | option ["-i", "--int"], :flag, "use signed 16 bit integers"
25 | option ["-d", "--dword"], :flag, "use unsigned 32 bit integers"
26 | option ["-f", "--float"], :flag, "use signed 32 bit floating point values"
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 host_option
41 | option ["-p", "--port"], 'PORT', "use TCP port", :default => 502
42 | end
43 |
44 | def output_option
45 | end
46 |
47 | def debug_option
48 | option ["-D", "--debug"], :flag, "show debug messages"
49 | end
50 |
51 | def timeout_option
52 | option ["-T", "--timeout"], 'TIMEOUT', "Specify the timeout in seconds when talking to the slave" do |t|
53 | Integer(t)
54 | end
55 | end
56 |
57 | def connect_timeout_option
58 | option ["-C", "--connect-timeout"], 'TIMEOUT', "Specify the timeout in seconds when connecting to TCP socket" do |t|
59 | Integer(t)
60 | end
61 | end
62 | end
63 |
64 |
65 | def data_size
66 | case addr_type
67 | when :bit, :word, :int
68 | 1
69 | when :float, :dword
70 | 2
71 | end
72 | end
73 |
74 |
75 | def addr_offset
76 | address[:offset]
77 | end
78 |
79 | def addr_type
80 | if int?
81 | :int
82 | elsif dword?
83 | :dword
84 | elsif float?
85 | :float
86 | elsif word?
87 | :word
88 | else
89 | address[:datatype]
90 | end
91 | end
92 |
93 | def addr_area
94 | address[:area]
95 | end
96 |
97 |
98 | def schneider_match(address)
99 | schneider_match = address.match /%M([FWD])?(\d+)/i
100 | if schneider_match
101 | {:offset => schneider_match[2].to_i, :format => :schneider}.tap do |result|
102 | case schneider_match[1]
103 | when nil
104 | result[:datatype] = :bit
105 | when 'W', 'w'
106 | result[:datatype] = :word
107 | result[:area] = :holding_registers
108 | when 'F', 'f'
109 | result[:datatype] = :float
110 | result[:area] = :holding_registers
111 | when 'D', 'd'
112 | result[:datatype] = :dword
113 | result[:area] = :holding_registers
114 | end
115 | end
116 | end
117 | end
118 |
119 |
120 | def modicon_match(address)
121 | if address.match /^\d+$/
122 | offset = address.to_i
123 | case offset
124 | when 1..99999
125 | { :offset => offset - 1,
126 | :datatype => :bit,
127 | :format => :modicon }
128 | # 100001..199999 inputs are not supported
129 | when 300001..399999
130 | { :offset => offset - 300001,
131 | :datatype => :word,
132 | :format => :modicon,
133 | :area => :input_registers }
134 | when 400001..499999
135 | { :offset => offset - 400001,
136 | :datatype => :word,
137 | :format => :modicon,
138 | :area => :holding_registers }
139 | end
140 | end
141 | end
142 |
143 | def addr_format
144 | if schneider?
145 | :schneider
146 | elsif modicon?
147 | :modicon
148 | else
149 | address[:format]
150 | end
151 | end
152 |
153 | def sliced_write_registers(sl, offset, data)
154 | (0..(data.count - 1)).each_slice(MAX_WRITE_WORDS) do |slice|
155 | result = sl.write_holding_registers(slice.first + offset, data.values_at(*slice))
156 | end
157 | end
158 |
159 | def sliced_write_coils(sl, offset, data)
160 | (0..(data.count - 1)).each_slice(MAX_WRITE_COILS) do |slice|
161 | result = sl.write_multiple_coils(slice.first + offset, data.values_at(*slice))
162 | end
163 | end
164 |
165 | end
166 | end
167 | end
168 |
169 |
--------------------------------------------------------------------------------
/lib/modbus-cli/dump_command.rb:
--------------------------------------------------------------------------------
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)' do |f|
11 | YAML.load_file(f).dup.tap do |ff|
12 | #parameter takes presedence
13 | ff[:host] = host || ff[:host]
14 | ff[:port] = port || ff[:port]
15 | ff[:slave] = slave || ff[:slave]
16 | ff[:offset] = offset || ff[:offset]
17 | end
18 | end
19 |
20 | option ["-h", "--host"], 'ADDR', "use the address/hostname ADDR instead of the stored one"
21 | host_option
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 | debug_option
33 |
34 | def execute
35 | host_ids = files_list.map {|d| d[:host] }.sort.uniq
36 | host_ids.each {|host_id| execute_host host_id }
37 | end
38 |
39 | def execute_host(host_id)
40 | slave_ids = files_list.select {|d| d[:host] == host_id }.map {|d| d[:slave] }.sort.uniq
41 | ModBus::TCPClient.connect(host_id, port) do |client|
42 | slave_ids.each {|slave_id| execute_slave host_id, slave_id, client }
43 | end
44 | end
45 |
46 | def execute_slave(host_id, slave_id, client)
47 | client.with_slave(slave_id) do |slave|
48 | slave.debug = true if debug?
49 | files_list.select {|d| d[:host] == host_id && d[:slave] == slave_id }.each do |file_data|
50 | execute_single_file slave, file_data
51 | end
52 | end
53 | end
54 |
55 | def execute_single_file(slave, file_data)
56 | address = modicon_match(file_data[:offset].to_s) || schneider_match(file_data[:offset].to_s)
57 | case address[:datatype]
58 | when :bit
59 | sliced_write_coils slave, address[:offset], file_data[:data]
60 | when :word
61 | sliced_write_registers slave, address[:offset], file_data[:data]
62 | end
63 | end
64 | end
65 | end
66 | end
67 |
68 |
--------------------------------------------------------------------------------
/lib/modbus-cli/read_command.rb:
--------------------------------------------------------------------------------
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 = 1000
11 | MAX_READ_WORD_COUNT = 125
12 |
13 | datatype_options
14 | format_options
15 | slave_option
16 | host_parameter
17 | host_option
18 | address_parameter
19 | option ["-o", "--output"], 'FILE', "write results to file FILE"
20 | debug_option
21 | timeout_option
22 | connect_timeout_option
23 |
24 | parameter 'COUNT', 'number of data to read', :attribute_name => :count do |c|
25 | result = Integer(c)
26 | raise ArgumentError, 'Count must be positive' if result <= 0
27 | result
28 |
29 | end
30 |
31 | def read_floats(sl)
32 | floats = read_and_unpack(sl, 'g')
33 | (0...count).each do |n|
34 | puts "#{ '%-10s' % address_to_s(addr_offset + n * data_size)} #{nice_float('% 16.8f' % floats[n])}"
35 | end
36 | end
37 |
38 | def read_dwords(sl)
39 | dwords = read_and_unpack(sl, 'N')
40 | (0...count).each do |n|
41 | puts "#{ '%-10s' % address_to_s(addr_offset + n * data_size)} #{'%10d' % dwords[n]}"
42 | end
43 | end
44 |
45 | def read_registers(sl, options = {})
46 | data = read_data_words(sl)
47 | if options[:int]
48 | data = data.pack('S').unpack('s')
49 | end
50 | read_range.zip(data).each do |pair|
51 | puts "#{ '%-10s' % address_to_s(pair.first)} #{'%6d' % pair.last}"
52 | end
53 | end
54 |
55 | def read_words_to_file(sl)
56 | write_data_to_file(read_data_words(sl))
57 | end
58 |
59 | def read_coils_to_file(sl)
60 | write_data_to_file(read_data_coils(sl))
61 | end
62 |
63 | def write_data_to_file(data)
64 | File.open(output, 'w') do |file|
65 | file.puts({ :host => host, :port => port, :slave => slave, :offset => address_to_s(addr_offset, :modicon), :data => data }.to_yaml)
66 | end
67 | end
68 |
69 | def read_coils(sl)
70 | data = read_data_coils(sl)
71 | read_range.zip(data) do |pair|
72 | puts "#{ '%-10s' % address_to_s(pair.first)} #{'%d' % pair.last}"
73 | end
74 | end
75 |
76 | def execute
77 | connect_lambda =
78 | if connect_timeout
79 | Proc.new { |&block| ModBus::TCPClient.connect(host, port, connect_timeout: connect_timeout, &block) }
80 | else
81 | Proc.new { |&block| ModBus::TCPClient.connect(host, port, &block) }
82 | end
83 |
84 |
85 | connect_lambda.call do |cl|
86 | cl.with_slave(slave) do |sl|
87 | sl.debug = true if debug?
88 | sl.read_retry_timeout = timeout if timeout
89 |
90 | if output then
91 | case addr_type
92 | when :bit
93 | read_coils_to_file(sl)
94 | else
95 | read_words_to_file(sl)
96 | end
97 | else
98 | case addr_type
99 | when :bit
100 | read_coils(sl)
101 | when :int
102 | read_registers(sl, :int => true)
103 | when :word
104 | read_registers(sl)
105 | when :float
106 | read_floats(sl)
107 | when :dword
108 | read_dwords(sl)
109 | end
110 | end
111 | end
112 | end
113 | end
114 |
115 | def read_and_unpack(sl, format)
116 | # the word ordering is wrong. calling reverse two times effectively swaps every pair
117 | read_data_words(sl).reverse.pack('n*').unpack("#{format}*").reverse
118 | end
119 |
120 | def read_data_words(sl)
121 | result = []
122 | read_range.each_slice(MAX_READ_WORD_COUNT) do |slice|
123 | case addr_area
124 | when :input_registers
125 | result += sl.read_input_registers(slice.first, slice.count)
126 | else # assume holding registers
127 | result += sl.read_holding_registers(slice.first, slice.count)
128 | end
129 | end
130 | result
131 | end
132 |
133 |
134 | def read_data_coils(sl)
135 | result = []
136 | read_range.each_slice(MAX_READ_COIL_COUNT) do |slice|
137 | result += sl.read_coils(slice.first, slice.count)
138 | end
139 | result
140 | end
141 |
142 | def read_range
143 | (addr_offset..(addr_offset + count * data_size - 1))
144 | end
145 |
146 |
147 | def nice_float(str)
148 | m = str.match /^(.*[.][0-9])0*$/
149 | if m
150 | m[1]
151 | else
152 | str
153 | end
154 | end
155 |
156 | def address_to_s(addr, format = addr_format)
157 | case format
158 | when :schneider
159 | case addr_type
160 | when :bit
161 | '%M' + addr.to_s
162 | when :word, :int
163 | '%MW' + addr.to_s
164 | when :dword
165 | '%MD' + addr.to_s
166 | when :float
167 | '%MF' + addr.to_s
168 | end
169 | when :modicon
170 | case addr_type
171 | when :bit
172 | (addr + 1).to_s
173 | when :word, :int, :dword, :float
174 | case addr_area
175 | when :input_registers
176 | (addr + 300001).to_s
177 | else # default :read_holding_registers
178 | (addr + 400001).to_s
179 | end
180 | end
181 | end
182 | end
183 | end
184 | end
185 | end
186 |
187 |
188 |
--------------------------------------------------------------------------------
/lib/modbus-cli/version.rb:
--------------------------------------------------------------------------------
1 | module Modbus
2 | module Cli
3 | VERSION = "0.0.16"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/modbus-cli/write_command.rb:
--------------------------------------------------------------------------------
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 | host_option
15 | address_parameter
16 | debug_option
17 | timeout_option
18 | connect_timeout_option
19 |
20 | parameter 'VALUES ...', 'values to write, nonzero counts as true for discrete values' do |v|
21 | case addr_type
22 |
23 | when :bit
24 | int_parameter v, 0, 1
25 | when :word
26 | int_parameter v, 0, 0xffff
27 | when :int
28 | int_parameter v, -32768, 32767
29 | when :dword
30 | int_parameter v, 0, 0xffffffff
31 | when :float
32 | Float(v)
33 | end
34 | end
35 |
36 |
37 |
38 | def execute
39 | connect_args =
40 | if connect_timeout
41 | [host, port, {connect_timeout: connect_timeout}]
42 | else
43 | [host, port]
44 | end
45 | ModBus::TCPClient.connect(*connect_args) do |cl|
46 | cl.with_slave(slave) do |sl|
47 | sl.debug = true if debug?
48 | sl.read_retry_timeout = timeout if timeout
49 |
50 | case addr_type
51 | when :bit
52 | write_coils sl
53 | when :word, :int
54 | write_words sl
55 | when :float
56 | write_floats sl
57 | when :dword
58 | write_dwords sl
59 | end
60 | end
61 | end
62 | end
63 |
64 | def write_coils(sl)
65 | sliced_write_coils sl, addr_offset, values_list
66 | end
67 |
68 | def write_words(sl)
69 | sliced_write_registers sl, addr_offset, values_list.pack('S*').unpack('S*')
70 | end
71 |
72 | def write_floats(sl)
73 | pack_and_write sl, 'g'
74 | end
75 |
76 | def write_dwords(sl)
77 | pack_and_write sl, 'N'
78 | end
79 |
80 | def pack_and_write(sl, format)
81 | # the word ordering is wrong. calling reverse two times effectively swaps every pair
82 | sliced_write_registers(sl, addr_offset, values_list.reverse.pack("#{format}*").unpack('n*').reverse)
83 | end
84 |
85 | def int_parameter(vv, min, max)
86 | Integer(vv).tap do |v|
87 | raise ArgumentError, "Value should be in the range #{min}..#{max}" unless (min..max).member? v
88 | end
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/modbus-cli.gemspec:
--------------------------------------------------------------------------------
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 | s.license = "MIT"
14 |
15 | s.rubyforge_project = "modbus-cli"
16 |
17 | s.files = `git ls-files`.split("\n")
18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20 | s.require_paths = ["lib"]
21 |
22 | # specify any dependencies here; for example:
23 | # s.add_development_dependency "rspec"
24 | s.add_runtime_dependency "rmodbus", '~> 2.1'
25 | s.add_runtime_dependency "clamp", '~> 1.3'
26 | s.add_runtime_dependency "gserver", '~> 0.0'
27 | s.add_development_dependency "rspec", '~> 3.13'
28 | s.add_development_dependency "rake", '~> 13.2'
29 | s.add_development_dependency "bundler", '~> 2.6'
30 | end
31 |
--------------------------------------------------------------------------------
/spec/dump_command_spec.rb:
--------------------------------------------------------------------------------
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 = double 'client'
12 | slave = double 'slave'
13 | YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :port => 502, :slave => 5, :offset => 400123, :data => [4, 5, 6])
14 | ModBus::TCPClient.should_receive(:connect).with('1.2.3.4', 502).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 = double 'client1'
22 | client2 = double 'client2'
23 | slave1 = double 'slave1'
24 | slave2 = double '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', 502).and_yield(client1)
29 | ModBus::TCPClient.should_receive(:connect).with('Y', 502).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 = double 'client1'
39 | slave1 = double 'slave1'
40 | slave2 = double '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', 502).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 = double 'client1'
54 | slave1 = double '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', 502).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 parameter' do
66 | YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :port => 502, :slave => 5, :offset => 123, :data => [4, 5, 6])
67 | ModBus::TCPClient.should_receive(:connect).with('Y', 502)
68 | cmd.run %w(dump --host Y dump.yml)
69 | end
70 |
71 | it 'accepts the --slave parameter' do
72 | client = double 'client'
73 | YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :port => 502, :slave => 5, :offset => 123, :data => [4, 5, 6])
74 | ModBus::TCPClient.should_receive(:connect).with('1.2.3.4', 502).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 parameter with modicon addressing' do
80 | client = double 'client'
81 | slave = double 'slave'
82 | YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :port => 502, :slave => 5, :offset => 123, :data => [4, 5, 6])
83 | ModBus::TCPClient.should_receive(:connect).with('1.2.3.4', 502).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 parameter with schneider addressing' do
90 | client = double 'client'
91 | slave = double 'slave'
92 | YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :port => 502, :slave => 5, :offset => 123, :data => [4, 5, 6])
93 | ModBus::TCPClient.should_receive(:connect).with('1.2.3.4', 502).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 |
99 | it 'has a --debug flag' do
100 | client = double 'client'
101 | slave = double 'slave'
102 | YAML.should_receive(:load_file).with('dump.yml').and_return(:host => '1.2.3.4', :port => 502, :slave => 5, :offset => 400123, :data => [4, 5, 6])
103 | ModBus::TCPClient.should_receive(:connect).with('1.2.3.4', 502).and_yield(client)
104 | client.should_receive(:with_slave).with(5).and_yield(slave)
105 | slave.should_receive(:write_holding_registers).with(122, [4, 5, 6])
106 | slave.should_receive(:debug=).with(true)
107 | cmd.run %w(dump --debug dump.yml)
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/spec/modbus_cli_spec.rb:
--------------------------------------------------------------------------------
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 |
25 |
--------------------------------------------------------------------------------
/spec/read_command_spec.rb:
--------------------------------------------------------------------------------
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 holding registers' do
14 | _client, slave = standard_connect_helper '1.2.3.4', 502
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 | it 'can read input registers' do
21 | _client, slave = standard_connect_helper '1.2.3.4', 502
22 | slave.should_receive(:read_input_registers).with(100, 10).and_return((0..9).to_a)
23 | cmd.run %w(read 1.2.3.4 300101 10)
24 | stdout.should match(/^\s*300106\s*5$/)
25 | end
26 |
27 | it 'can read floating point numbers' do
28 | _client, slave = standard_connect_helper '1.2.3.4', 502
29 | slave.should_receive(:read_holding_registers).with(100, 4).and_return([52429, 17095, 52429, 17095])
30 | cmd.run %w(read 1.2.3.4 %MF100 2)
31 | stdout.should match(/^\s*%MF102\s*99[.]9(00[0-9]*)?$/)
32 | end
33 |
34 | it 'should display numbers with good precision' do
35 | _client, slave = standard_connect_helper '1.2.3.4', 502
36 | slave.should_receive(:read_holding_registers).and_return([1049, 16286])
37 | cmd.run %w(read 1.2.3.4 %MF100 1)
38 | stdout.should match(/^\s*%MF100\s*1[.]2345\d*$/)
39 | end
40 |
41 | it 'can read double word numbers' do
42 | _client, slave = standard_connect_helper '1.2.3.4', 502
43 | slave.should_receive(:read_holding_registers).with(100, 4).and_return([16959, 15, 16959, 15])
44 | cmd.run %w(read 1.2.3.4 %MD100 2)
45 | stdout.should match(/^\s*%MD102\s*999999$/)
46 | end
47 |
48 | it 'can read coils' do
49 | _client, slave = standard_connect_helper '1.2.3.4', 502
50 | slave.should_receive(:read_coils).with(100, 10).and_return([1, 0] * 5)
51 | cmd.run %w(read 1.2.3.4 %M100 10)
52 | stdout.should match(/^\s*%M105\s*0$/)
53 | end
54 |
55 |
56 |
57 |
58 | it 'rejects illegal counts' do
59 | lambda { cmd.run %w(read 1.2.3.4 %MW100 1+0) }.should raise_exception(Clamp::UsageError)
60 | lambda { cmd.run %w(read 1.2.3.4 %MW100 -10) }.should raise_exception(Clamp::UsageError)
61 | lambda { cmd.run %w(read 1.2.3.4 %MW100 9.9) }.should raise_exception(Clamp::UsageError)
62 | end
63 |
64 | it 'rejects illegal addresses' do
65 | lambda { cmd.run %w(read 1.2.3.4 %MW1+00) }.should raise_exception(Clamp::UsageError)
66 | end
67 |
68 | it 'should split large reads into smaller chunks for words' do
69 | _client, slave = standard_connect_helper '1.2.3.4', 502
70 | slave.should_receive(:read_holding_registers).with(100, 125).and_return([1, 0] * 1000)
71 | slave.should_receive(:read_holding_registers).with(225, 25).and_return([1, 0] * 1000)
72 | cmd.run %w(read 1.2.3.4 %MW100 150)
73 | end
74 |
75 | it 'should split large reads into smaller chunks for coils' do
76 | _client, slave = standard_connect_helper '1.2.3.4', 502
77 | slave.should_receive(:read_coils).with(100, 1000).and_return([1, 0] * 500)
78 | slave.should_receive(:read_coils).with(1100, 1000).and_return([1, 0] * 500)
79 | slave.should_receive(:read_coils).with(2100, 1000).and_return([1, 0] * 500)
80 | cmd.run %w(read 1.2.3.4 %M100 3000)
81 | end
82 |
83 | it 'can read registers as ints' do
84 | _client, slave = standard_connect_helper '1.2.3.4', 502
85 | slave.should_receive(:read_holding_registers).with(100, 1).and_return([0xffff])
86 | cmd.run %w(read --int 1.2.3.4 %MW100 1)
87 | stdout.should match(/^\s*%MW100\s*-1$/)
88 | end
89 |
90 | it 'can read registers as floats' do
91 | _client, slave = standard_connect_helper '1.2.3.4', 502
92 | slave.should_receive(:read_holding_registers).with(100, 2).and_return([0,0])
93 | cmd.run %w(read --float 1.2.3.4 %MW100 1)
94 | end
95 |
96 | it 'can read registers as dwords' do
97 | _client, slave = standard_connect_helper '1.2.3.4', 502
98 | slave.should_receive(:read_holding_registers).with(100, 2).and_return([0,0])
99 | cmd.run %w(read --dword 1.2.3.4 %MW100 1)
100 | end
101 |
102 | it 'can read registers as words' do
103 | _client, slave = standard_connect_helper '1.2.3.4', 502
104 | slave.should_receive(:read_holding_registers).with(100, 1).and_return([0])
105 | cmd.run %w(read --word 1.2.3.4 %MD100 1)
106 | end
107 |
108 | it 'accepts Modicon addresses for coils' do
109 | _client, slave = standard_connect_helper '1.2.3.4', 502
110 | slave.should_receive(:read_coils).with(100, 10).and_return([1, 0] * 5)
111 | cmd.run %w(read 1.2.3.4 101 10)
112 | stdout.should match(/^\s*106\s*0$/)
113 | end
114 |
115 | it 'accepts Modicon addresses for registers' do
116 | _client, slave = standard_connect_helper '1.2.3.4', 502
117 | slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
118 | cmd.run %w(read 1.2.3.4 400101 10)
119 | stdout.should match(/^\s*400106\s*5$/)
120 | end
121 |
122 |
123 | it 'should accept the --modicon option to force modicon output' do
124 | _client, slave = standard_connect_helper '1.2.3.4', 502
125 | slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
126 | cmd.run %w(read --modicon 1.2.3.4 %MW100 10)
127 | stdout.should match(/^\s*400106\s*5$/)
128 | end
129 |
130 | it 'should accept the --schneider option to force schneider output' do
131 | _client, slave = standard_connect_helper '1.2.3.4', 502
132 | slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
133 | cmd.run %w(read --schneider 1.2.3.4 400101 10)
134 | stdout.should match(/^\s*%MW105\s*5$/)
135 | end
136 |
137 | it 'has a --slave parameter' do
138 | client = double 'client'
139 | ModBus::TCPClient.should_receive(:connect).with('X', 502).and_yield(client)
140 | client.should_receive(:with_slave).with(99)
141 | cmd.run %w(read --slave 99 X 101 1)
142 | end
143 |
144 | it 'can write the output from reading registers to a yaml file using the -o parameter' do
145 | _client, slave = standard_connect_helper '1.2.3.4', 502
146 | slave.should_receive(:read_holding_registers).with(100, 1).and_return([1])
147 | file_double = double('file')
148 | File.should_receive(:open).and_yield(file_double)
149 | file_double.should_receive(:puts).with({:host => '1.2.3.4', :port => 502, :slave => 1, :offset => '400101', :data => [1]}.to_yaml)
150 | cmd.run %w(read --output filename.yml 1.2.3.4 %MW100 1)
151 | stdout.should_not match(/./)
152 | end
153 |
154 | it 'can write the output from reading coils to a yaml file using the -o parameter' do
155 | _client, slave = standard_connect_helper '1.2.3.4', 502
156 | slave.should_receive(:read_coils).with(100, 1).and_return([1])
157 | file_double = double('file')
158 | File.should_receive(:open).and_yield(file_double)
159 | file_double.should_receive(:puts).with({:host => '1.2.3.4', :port => 502, :slave => 1, :offset => '101', :data => [1]}.to_yaml)
160 | cmd.run %w(read --output filename.yml 1.2.3.4 %M100 1)
161 | stdout.should_not match(/./)
162 | end
163 |
164 | it 'has a --debug flag' do
165 | _client, slave = standard_connect_helper '1.2.3.4', 502
166 | slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
167 | slave.should_receive(:debug=).with(true)
168 | cmd.run %w(read --debug 1.2.3.4 %MW100 10)
169 | end
170 |
171 | it 'has a --timeout flag' do
172 | _client, slave = standard_connect_helper '1.2.3.4', 502
173 | slave.should_receive(:read_retry_timeout=).with(99)
174 | slave.should_receive(:read_holding_registers).with(100, 10).and_return((0..9).to_a)
175 | cmd.run %w(read --timeout 99 1.2.3.4 %MW100 10)
176 | end
177 |
178 | it 'has a --connect-timeout flag' do
179 | ModBus::TCPClient.should_receive(:connect).with('X', 502, connect_timeout: 99)
180 | cmd.run %w(read --connect-timeout 99 X %MW100 10)
181 | end
182 | end
183 |
184 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'clamp'
3 | require 'stringio'
4 | require 'modbus-cli'
5 |
6 | # keep old fashioned should syntax for now, would be better to convert to new
7 | # syntax
8 | RSpec.configure do |config|
9 | config.mock_with :rspec do |mocks|
10 | mocks.syntax = [:should, :expect]
11 | end
12 | config.expect_with :rspec do |c|
13 | c.syntax = [:should, :expect]
14 | end
15 | end
16 |
17 | # Borrowed from Clamp tests
18 | module OutputCapture
19 |
20 | def self.included(target)
21 | target.before do
22 | $stdout = @out = StringIO.new
23 | $stderr = @err = StringIO.new
24 | end
25 | target.after do
26 | $stdout = STDOUT
27 | $stderr = STDERR
28 | end
29 | end
30 |
31 | def stdout
32 | @out.string
33 | end
34 |
35 | def stderr
36 | @err.string
37 | end
38 |
39 |
40 |
41 | end
42 |
43 | def stub_tcpip
44 | # prevent any real TCP communications
45 | allow(TCPSocket).to receive(:new).and_return("TCPSocket")
46 | end
47 |
48 |
49 | def standard_connect_helper(address, port)
50 | client = double 'client'
51 | slave = double 'slave'
52 | ModBus::TCPClient.should_receive(:connect).with(address, port).and_yield(client)
53 | client.should_receive(:with_slave).with(1).and_yield(slave)
54 | return client, slave
55 | end
56 |
57 |
58 | def cmd
59 | Modbus::Cli::CommandLineRunner.new('modbus-cli')
60 | end
61 |
--------------------------------------------------------------------------------
/spec/write_command_spec.rb:
--------------------------------------------------------------------------------
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', 502
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', 502
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', 502
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', 502
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', 502
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', 502
56 | slave.should_receive(:write_multiple_coils).with(100, [0, 1] * 400)
57 | slave.should_receive(:write_multiple_coils).with(900, [0, 1] * 400)
58 | slave.should_receive(:write_multiple_coils).with(1700, [0, 1] * 200)
59 | cmd.run %w(write HOST %M100) + [0, 1] * 1000
60 | end
61 |
62 | it 'can write to registers as ints' do
63 | _client, slave = standard_connect_helper 'HOST', 502
64 | slave.should_receive(:write_holding_registers).with(100, [0xffff])
65 | cmd.run %w(write --int HOST %MW100 -1)
66 | end
67 |
68 | it 'can write to registers as floats' do
69 | _client, slave = standard_connect_helper 'HOST', 502
70 | slave.should_receive(:write_holding_registers).with(100, [52429, 17095])
71 | cmd.run %w(write --float HOST %MW100 99.9)
72 | end
73 |
74 | it 'can write to registers as double words' do
75 | _client, slave = standard_connect_helper 'HOST', 502
76 | slave.should_receive(:write_holding_registers).with(100, [16959, 15])
77 | cmd.run %w(write --dword HOST %MW100 999999)
78 | end
79 |
80 | it 'can write to registers as words' do
81 | _client, slave = standard_connect_helper 'HOST', 502
82 | slave.should_receive(:write_holding_registers).with(100, [99])
83 | cmd.run %w(write --word HOST %MF100 99)
84 | end
85 |
86 | it 'can write to registers using Modicon addressing' do
87 | _client, slave = standard_connect_helper 'HOST', 502
88 | slave.should_receive(:write_holding_registers).with(100, [1, 2, 3, 4])
89 | cmd.run %w(write HOST 400101 1 2 3 4)
90 | end
91 |
92 | it 'can write to coils using Modicon addressing' do
93 | _client, slave = standard_connect_helper 'HOST', 502
94 | slave.should_receive(:write_multiple_coils).with(100, [1, 0, 1, 0, 1, 0, 0, 1, 1])
95 | cmd.run %w(write HOST 101 1 0 1 0 1 0 0 1 1)
96 | end
97 |
98 | it 'has a --slave parameter' do
99 | client = double 'client'
100 | ModBus::TCPClient.should_receive(:connect).with('X', 502).and_yield(client)
101 | client.should_receive(:with_slave).with(99)
102 | cmd.run %w(write --slave 99 X 101 1)
103 | end
104 |
105 | it 'has a --debug flag' do
106 | _client, slave = standard_connect_helper 'HOST', 502
107 | slave.should_receive(:debug=).with(true)
108 | slave.should_receive(:write_multiple_coils)
109 | cmd.run %w(write --debug HOST 101 1)
110 | end
111 |
112 | it 'has a --timeout flag' do
113 | _client, slave = standard_connect_helper 'HOST', 502
114 | slave.should_receive(:read_retry_timeout=).with(99)
115 | slave.should_receive(:write_multiple_coils)
116 | cmd.run %w(write --timeout 99 HOST 101 1)
117 | end
118 |
119 | it 'has a --connect-timeout flag' do
120 | ModBus::TCPClient.should_receive(:connect).with('X', 502, {connect_timeout: 99})
121 | cmd.run %w(write --connect-timeout 99 X %MW100 10)
122 | end
123 | end
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------