tallakt-plcutil 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2009-01-28
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,21 @@
1
+ History.txt
2
+ LICENSE
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ TODO
7
+ bin/awlpp
8
+ bin/s7tointouch
9
+ lib/plcutil/siemens/awlfile.rb
10
+ lib/plcutil/siemens/awlprettyprintrunner.rb
11
+ lib/plcutil/siemens/step7tointouchrunner.rb
12
+ lib/plcutil/wonderware/intouchfile.rb
13
+ lib/plcutil/wonderware/intouchreader.rb
14
+ lib/plcutil/wonderware/standard_sections.yaml
15
+ lib/plcutil.rb
16
+ script/console
17
+ script/console.cmd
18
+ script/destroy
19
+ script/destroy.cmd
20
+ script/generate
21
+ script/generate.cmd
data/README.rdoc ADDED
@@ -0,0 +1,61 @@
1
+ = plcutil
2
+
3
+ http://github.com/tallakt/plcutil
4
+
5
+ == DESCRIPTION:
6
+
7
+ A set of command line utilities and helper classes to convert between IO list files of different PLC types into different HMI types.
8
+
9
+ Currently Siemens and Wonderware are supported, Schneider and Wonderware IAS support planned in the near future.
10
+
11
+ This is still a quite rough version - pre alpha!!
12
+
13
+ == FEATURES/PROBLEMS:
14
+
15
+
16
+ == SYNOPSIS:
17
+
18
+ Print the contents of Siemens Step 7 IO file
19
+
20
+ awlpp -s SYMLIST.DBF 00000007.AWL
21
+
22
+ Convert a siemens IO list into a Wonderware Intouch (non IAS) DB import file
23
+
24
+ s7tointouch -s SYMLIST.DBF -a SIEMENSPLC 00000007.AWL
25
+
26
+ The commands support the --help parameter for a full list of parameters
27
+
28
+
29
+ == REQUIREMENTS:
30
+
31
+ * FIX (list of requirements)
32
+
33
+ == INSTALL:
34
+
35
+ gem install plcutil
36
+
37
+
38
+ == LICENSE:
39
+
40
+ (The MIT License)
41
+
42
+ Copyright (c) 2009 Tallak Tveide
43
+
44
+ Permission is hereby granted, free of charge, to any person obtaining
45
+ a copy of this software and associated documentation files (the
46
+ 'Software'), to deal in the Software without restriction, including
47
+ without limitation the rights to use, copy, modify, merge, publish,
48
+ distribute, sublicense, and/or sell copies of the Software, and to
49
+ permit persons to whom the Software is furnished to do so, subject to
50
+ the following conditions:
51
+
52
+ The above copyright notice and this permission notice shall be
53
+ included in all copies or substantial portions of the Software.
54
+
55
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
56
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
57
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
58
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
59
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
60
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
61
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/plcutil'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('plcutil', Plcutil::VERSION) do |p|
7
+ p.developer('Tallak Tveide', 'tallak@tveide.net')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ p.rubyforge_name = p.name # TODO this is default value
10
+ # p.extra_deps = [
11
+ # ['activesupport','>= 2.0.2'],
12
+ # ]
13
+ p.extra_dev_deps = [
14
+ ['newgem', ">= #{::Newgem::VERSION}"]
15
+ ]
16
+
17
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
18
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
19
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
20
+ p.rsync_args = '-av --delete --ignore-errors'
21
+ end
22
+
23
+ require 'newgem/tasks' # load /tasks/*.rake
24
+ Dir['tasks/**/*.rake'].each { |t| load t }
25
+
26
+ # TODO - want other tests/tasks run by default? Add them to the list
27
+ # task :default => [:spec, :features]
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ - Create gemspec
2
+ - README describing usage
3
+ - RDoc comments
4
+ - RSpec tests
data/bin/awlpp ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'plcutil/siemens/awlprettyprintrunner'
4
+ require 'plcutil/siemens/step7tointouchrunner'
5
+
6
+ # try: awlpp --help
7
+ PlcUtil::AwlPrettyPrintRunner.new ARGV
8
+
data/bin/s7tointouch ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'plcutil/siemens/step7tointouchrunner'
4
+
5
+ # try: s7tointouch --help
6
+ PlcUtil::Step7ToIntouchRunner.new ARGV
7
+
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'rubygems'
4
+ require 'dbf'
5
+
6
+ module PlcUtil
7
+
8
+ # Reads a Siemens Step 7 AWL file and creates single tags with addresses and comment
9
+ class AwlFile
10
+ def initialize(filename, options = {})
11
+ @types = {}
12
+ init_basic_types
13
+ @datablocks = []
14
+ @symlist = {}
15
+
16
+ if options[:symlist]
17
+ throw 'Specified symlist file not found' unless File.exists? options[:symlist]
18
+ table = DBF::Table.new(options[:symlist])
19
+ table.records.each do |rec|
20
+ @symlist[rec.attributes['_skz']] = rec.attributes['_opiec'] # or _ophist or _datatyp
21
+ end
22
+ end
23
+
24
+ @symlist.merge!(options[:blocks] || {}) # User may override blocks
25
+
26
+ File.open filename do |f|
27
+ parse f
28
+ end
29
+ end
30
+
31
+ def each_tag
32
+ @datablocks.each do |var|
33
+ var.type.explode(Address.new(0,0), var.name).each do |item|
34
+ yield item[:name],
35
+ complete_address(var.name, item[:addr].to_s),
36
+ item[:comment],
37
+ item[:struct_comment],
38
+ item[:type].downcase.to_sym
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def data_block_address(name)
46
+ if @symlist.key? name
47
+ @symlist[name].gsub /\s+/, ''
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ def complete_address(data_block_name, address)
54
+ [data_block_address(data_block_name), address].select {|s| s}.join ','
55
+ end
56
+
57
+ def init_basic_types
58
+ [BoolType.new, BasicType.new('INT', 2), BasicType.new('REAL', 4)].each do |basic|
59
+ add_type basic
60
+ end
61
+ end
62
+
63
+ def add_type(type)
64
+ @types[type.name] = type
65
+ end
66
+
67
+ def lookup_type(type)
68
+ throw "Could not find type '#{type}'" unless @types.key? type
69
+ @types[type]
70
+ end
71
+
72
+ def parse(file)
73
+ stack = []
74
+ in_array_decl = false
75
+ tagname = start = stop = type = comment = nil
76
+ file.each_line do |l|
77
+ l.chomp!
78
+ if in_array_decl
79
+ if l.match '^\s+\"?([A-Za-z0-9_]+)"?\s?;'
80
+ type = $1
81
+ stack.last.add Variable.new(tagname, ArrayType.new(@types[type], start..stop), comment)
82
+ end
83
+ in_array_decl = false
84
+ else
85
+ in_array_decl = false
86
+ case l
87
+ when /^TYPE "([^"]+)/
88
+ stack = [StructType.new $1, :datatype]
89
+ add_type stack.last
90
+ when /^DATA_BLOCK "([^"]+)/
91
+ stack = [StructType.new $1, :datablock]
92
+ @datablocks << Variable.new($1, stack.last)
93
+ when /^\s*(\S+) : STRUCT /
94
+ s = StructType.new 'STRUCT', :anonymous
95
+ stack.last.add Variable.new $1, s
96
+ stack << stack.last.children.last.type
97
+ when /^\s+END_STRUCT/
98
+ stack.pop
99
+ when /^\s+([A-Za-z0-9_]+) : "?([A-Za-z0-9_]+)"?\s*(:=\s*[0-9.e+-]+)?;(\s*\/\/(.*))?/
100
+ # New variable in struct or data block
101
+ tagname, type_name, comment = $1, $2, $5
102
+ stack.last.add Variable.new(tagname, lookup_type(type_name), comment)
103
+ when /^\s+([A-Za-z0-9_]+) : ARRAY\s*\[(\d+)\D+(\d+) \] OF "?([A-Za-z0-9_]+)"?\s?;(\s*\/\/(.*))?/
104
+ tagname, start, stop, type, comment = $1, $2, $3, $4, $6
105
+ stack.last.add Variable.new(tagname, ArrayType.new(lookup_type(type), start..stop), comment)
106
+ when /^\s+([A-Za-z0-9_]+) : ARRAY\s*\[(\d+)\D+(\d+) \] OF(\s?\/\/(.*))?$/
107
+ tagname, start, stop, comment = $1, $2, $3, $4
108
+ in_array_decl = true
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ def defined_type(type)
115
+ @types.key?(type.name)
116
+ end
117
+
118
+
119
+ class BasicType
120
+ attr_accessor :size, :name
121
+
122
+ def initialize(name, size)
123
+ @size, @name = size, name
124
+ end
125
+
126
+ def explode(start_addr, name, comment, struct_comment)
127
+ [:addr => start_addr.first_even_bit, :name => name,
128
+ :struct_comment => struct_comment, :comment => comment, :type => @name]
129
+ end
130
+
131
+ def end_address(start_address)
132
+ start_address.first_even_bit.skip!(size)
133
+ end
134
+ end
135
+
136
+
137
+ class BoolType
138
+ attr_accessor :name
139
+
140
+ def initialize
141
+ @name = 'BOOL'
142
+ end
143
+
144
+ def explode(start_addr, name, comment, struct_comment)
145
+ [:addr => start_addr, :name => name,
146
+ :struct_comment => struct_comment, :comment => comment, :type => @name]
147
+ end
148
+
149
+ def end_address(start_address)
150
+ start_address.next_bit
151
+ end
152
+ end
153
+
154
+ class StructType
155
+ attr_accessor :name, :children, :type
156
+
157
+ def initialize(name = 'STRUCT', type = :anonymous)
158
+ @name = name
159
+ @children = []
160
+ @type = type
161
+ end
162
+
163
+ def add(child)
164
+ throw 'Added nil child' unless child
165
+ throw 'Added nil child type' unless child.type
166
+ @children << child
167
+ end
168
+
169
+ def end_address(start_address)
170
+ addr = start_address.first_even_bit
171
+ @children.each do |child|
172
+ addr = child.type.end_address addr
173
+ end
174
+ addr.first_bit!
175
+ end
176
+
177
+ def explode(start_addr, name, comment = nil, struct_comment = nil)
178
+ addr = start_addr.first_even_bit
179
+ exploded = []
180
+ @children.each do |child|
181
+ exploded += child.type.explode(addr, name + '.' + child.name, child.comment,
182
+ type == :datablock ? child.comment : struct_comment)
183
+ addr = child.type.end_address addr
184
+ end
185
+ exploded
186
+ end
187
+
188
+ end
189
+
190
+ class ArrayType
191
+ attr_accessor :range, :type
192
+
193
+ def initialize(type, range)
194
+ throw 'Added nil array type' unless type
195
+ throw 'Added nil array range' unless range
196
+ @range, @type = range, type
197
+ end
198
+
199
+ def name
200
+ 'ARRAY'
201
+ end
202
+
203
+ def end_address(start_address)
204
+ addr = start_address
205
+ range.each do
206
+ addr = type.end_address addr
207
+ end
208
+ addr
209
+ end
210
+
211
+ def explode(start_addr, name, comment, struct_comment)
212
+ exploded = []
213
+ addr = start_addr
214
+ range.to_a.each_with_index do |v, i|
215
+ exploded += type.explode(addr, name + '[' + v.to_s + ']', comment, struct_comment)
216
+ addr = type.end_address(addr)
217
+ end
218
+ exploded
219
+ end
220
+ end
221
+
222
+ class Variable
223
+ attr_accessor :name, :type, :comment
224
+
225
+ def initialize(name, type, comment = nil)
226
+ @name, @type, @comment= name, type, comment
227
+ end
228
+ end
229
+
230
+ class Address
231
+ attr_accessor :byte, :bit
232
+
233
+ def initialize(byte, bit)
234
+ @byte, @bit = byte, bit
235
+ end
236
+
237
+ def to_s
238
+ @byte.to_s + '.' + @bit.to_s
239
+ end
240
+
241
+ def first_bit!
242
+ @byte += 1 if @bit > 0
243
+ @bit = 0
244
+ self
245
+ end
246
+
247
+ def first_bit
248
+ self.clone.first_bit!
249
+ end
250
+
251
+ def first_even_bit!
252
+ first_bit!
253
+ skip! if @byte % 2 > 0
254
+ self
255
+ end
256
+
257
+ def first_even_bit
258
+ self.clone.first_even_bit!
259
+ end
260
+
261
+ def next
262
+ self.clone.next!
263
+ end
264
+
265
+ def next_bit!
266
+ @bit += 1
267
+ if @bit == 8
268
+ @bit = 0
269
+ @byte += 1
270
+ end
271
+ self
272
+ end
273
+
274
+ def next_bit
275
+ self.clone.next_bit!
276
+ end
277
+
278
+ def skip!(bytes = 1)
279
+ (bytes * 8).times do
280
+ next_bit!
281
+ end
282
+ self
283
+ end
284
+
285
+ def skip(bytes = 1)
286
+ self.clone.skip bytes
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'optparse'
4
+ require 'plcutil/siemens/awlfile'
5
+
6
+ module PlcUtil
7
+ # Command line tool to read and output an awl file
8
+ class AwlPrettyPrintRunner
9
+ def initialize(args)
10
+ @awloptions = {}
11
+ @format = '%-11s %-40s%-10s%s'
12
+ @commentformat = '# %s / %s'
13
+ @output = nil
14
+ @symlistfile = nil
15
+ option_parser.parse! args
16
+ if args.size != 1
17
+ show_help
18
+ exit
19
+ end
20
+ filename, = args
21
+ @awl = AwlFile.new filename, @awloptions
22
+ if @output
23
+ File.open @output, 'w' do |f|
24
+ print_to_file f
25
+ end
26
+ else
27
+ print_to_file $stdout
28
+ end
29
+ end
30
+
31
+
32
+ def print_to_file(f)
33
+ @awl.each_tag do |name, addr, comment, struct_comment, type|
34
+ f.puts @format % [
35
+ addr,
36
+ name,
37
+ type.to_s,
38
+ [comment, struct_comment].compact! ? '' : (@commentformat % [comment, struct_comment])
39
+ ]
40
+ end
41
+ end
42
+
43
+ def option_parser
44
+ OptionParser.new do |opts|
45
+ opts.banner = "Usage: awlpp [options] AWLFILE"
46
+ opts.on("-c", "--csv", String, "Output as CSV file") do
47
+ format = '%s;%s;%s;%s'
48
+ @commentformat = '%s'
49
+ end
50
+ opts.on("-s", "--symlist FILE", String, "Specify SYMLIST.DBF file from S7 project ") do |symlistfile|
51
+ @awloptions[:symlist] = symlistfile
52
+ end
53
+ opts.on("-b", "--block NAME=ADDR", String, "Define address of datablock without", "reading symlist") do |blockdef|
54
+ name, addr = blockdef.split(/=/)
55
+ @awloptions[:blocks] ||= {}
56
+ @awloptions[:blocks][name] = addr
57
+ end
58
+ opts.on("-o", "--output FILE", String, "Output to specified file instead of", "standard output") do |output|
59
+ @output = output
60
+ end
61
+ opts.on_tail("-h", "--help", "Show this message") do
62
+ puts opts
63
+ exit
64
+ end
65
+ end
66
+ end
67
+
68
+ def show_help
69
+ puts option_parser
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'optparse'
4
+ require 'plcutil/siemens/awlfile'
5
+ require 'plcutil/wonderware/intouchfile'
6
+
7
+ module PlcUtil
8
+ # Command line tool to read and output an awl file
9
+ class Step7ToIntouchRunner
10
+ def initialize(command_line_arguments)
11
+ # standard command line options
12
+ @awloptions = {}
13
+ @intouchoptions = {}
14
+ @output = nil
15
+ @symlistfile = nil
16
+ @no_block = false
17
+ @access_name = nil
18
+ @filter_file = nil
19
+
20
+ # Parse command line options
21
+ option_parser.parse! command_line_arguments
22
+ if command_line_arguments.empty?
23
+ show_help
24
+ exit
25
+ end
26
+
27
+ # Read Siemens S7 file
28
+ @awllist = command_line_arguments.map{|filename| AwlFile.new filename, @awloptions}
29
+
30
+ # create a lookup table for used tags in the file to prevent duplicate ids
31
+ @used_tags = {}
32
+ @awllist.each do |awl|
33
+ awl.each_tag do |tag, addr, comment, type|
34
+ @used_tags[siemens_to_ww_tagname_long tag] = true
35
+ end
36
+ end
37
+
38
+ # load filter file to enable override of functions filter_comment_format and filter_handle_tag
39
+ load @filter_file if @filter_file && File.exists?(@filter_file)
40
+
41
+ # Write to intouch file
42
+ if @output
43
+ File.open @output, 'w' do |f|
44
+ print_to_file f
45
+ end
46
+ else
47
+ print_to_file $stdout
48
+ end
49
+ end
50
+
51
+ # This function may be overriden in filter ruby file
52
+ def filter_comment_format(comment, struct_comment)
53
+ if comment || struct_comment
54
+ [comment, struct_comment].compact.join(' / ').gsub(/"/, '')
55
+ else
56
+ ''
57
+ end
58
+ end
59
+
60
+ # This function may be overridden in filter ruby file
61
+ def filter_handle_tag(name, addr, comment, struct_comment, type, intouch_file)
62
+ ww_name = siemens_to_ww_tagname name
63
+ cc = filter_comment_format comment, struct_comment
64
+ case type
65
+ when :bool
66
+ intouch_file.new_io_disc(ww_name) do |io|
67
+ io.item_name = addr
68
+ io.comment = cc
69
+ end
70
+ when :int
71
+ intouch_file.new_io_int(ww_name) do |io|
72
+ io.item_name = addr
73
+ io.comment = cc
74
+ end
75
+ when :real
76
+ intouch_file.new_io_real(ww_name) do |io|
77
+ io.item_name = addr
78
+ io.comment = cc
79
+ end
80
+ else
81
+ throw RuntimeException.new 'Unsupported type found: ' + type.to_s
82
+ end
83
+ end
84
+
85
+
86
+ # This function may be overridden in filter ruby file
87
+ def filter_handle_awl_files
88
+ @awlfile.each do |awl|
89
+ awl.each_tag do |name, addr, comment, struct_comment, type|
90
+ filter_handle_tag name, addr, comment, struct_comment, type, @intouchfile
91
+ end
92
+ end
93
+ end
94
+
95
+
96
+ def print_to_file(f)
97
+ @intouchfile = IntouchFile.new nil, @intouchoptions
98
+ filter_handle_awl_files
99
+ i.write_csv f
100
+ end
101
+
102
+ def siemens_to_ww_tagname(s)
103
+ new_unique_tag(siemens_to_ww_tagname_long s)
104
+ end
105
+
106
+ def siemens_to_ww_tagname_long(s)
107
+ if @no_block
108
+ s.gsub /^[^\.]*./, ''
109
+ else
110
+ s
111
+ end.gsub(/\./, '_').gsub(/\[(\d+)\]/) { '_' + $1 }
112
+ end
113
+
114
+ def new_unique_tag_helper(s, n)
115
+ s[0..(31 - n.to_s.size - 1)] + '%' + n.to_s
116
+ end
117
+
118
+ def new_unique_tag(s)
119
+ if s.size < 33
120
+ s
121
+ else
122
+ n = nil
123
+ new_tag = new_unique_tag_helper s, n
124
+ while @used_tags.key? new_tag
125
+ n ||= 0
126
+ n += 1
127
+ new_tag = new_unique_tag_helper s, n
128
+ end
129
+ @used_tags[new_tag] = true
130
+ new_tag
131
+ end
132
+ end
133
+
134
+ def option_parser
135
+ OptionParser.new do |opts|
136
+ opts.banner = "Usage: s7tointouch [options] AWLFILE [AWLFILE ...]"
137
+ opts.on("-s", "--symlist FILE", String, "Specify SYMLIST.DBF file from S7 project ") do |symlistfile|
138
+ @awloptions[:symlist] = symlistfile
139
+ end
140
+ opts.on("-n", "--no-block", String, "Dont use the datablock as part of the tag", "name") do
141
+ @no_block = true
142
+ end
143
+ opts.on("-b", "--block NAME=ADDR", String, "Define address of datablock without", "reading ymlist") do |blockdef|
144
+ name, addr = blockdef.split(/=/)
145
+ @awloptions[:blocks] ||= {}
146
+ @awloptions[:blocks][name] = addr
147
+ end
148
+ opts.on("-a", "--access ACCESSNAME", String, "Set access name for all tags") do |access_name|
149
+ @intouchoptions[:access_name] = access_name
150
+ end
151
+ opts.on("-f", "--filter FILTER_RUBY_FILE", String, "Specify ruby filter file to override", "filter functions") do |filter_file|
152
+ @filter_file = filter_file
153
+ end
154
+ opts.on("-o", "--output FILE", String, "Output to specified file instead of", "standard output") do |output|
155
+ @output = output
156
+ end
157
+ opts.on_tail("-h", "--help", "Show this message") do
158
+ puts opts
159
+ exit
160
+ end
161
+ end
162
+ end
163
+
164
+ def show_help
165
+ puts option_parser
166
+ end
167
+ end
168
+ end
169
+