├── .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 | [![Build Status](https://travis-ci.org/tallakt/modbus-cli.svg?branch=master)](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 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
Data typeData sizeSchneider addressModicon addressParameter
word (default, unsigned)16 bits%MW100400101--word
integer (signed)16 bits%MW100400101--int
floating point32 bits%MF100400101--float
double word32 bits%MD100400101--dword
boolean (coils)1 bit%M100101N/A
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 | --------------------------------------------------------------------------------