modbus-cli 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.markdown +212 -0
- data/Rakefile +7 -0
- data/bin/modbus +7 -0
- data/lib/modbus-cli.rb +33 -0
- data/lib/modbus-cli/commands_common.rb +130 -0
- data/lib/modbus-cli/dump_command.rb +64 -0
- data/lib/modbus-cli/read_command.rb +160 -0
- data/lib/modbus-cli/version.rb +5 -0
- data/lib/modbus-cli/write_command.rb +81 -0
- data/modbus-cli.gemspec +26 -0
- data/spec/dump_command_spec.rb +104 -0
- data/spec/modbus_cli_spec.rb +24 -0
- data/spec/read_command_spec.rb +156 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/write_command_spec.rb +109 -0
- metadata +97 -0
data/Gemfile
ADDED
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
data/bin/modbus
ADDED
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,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
|
data/modbus-cli.gemspec
ADDED
@@ -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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|