tallakt-plcutil 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+