netlinx-workspace 0.3.0 → 1.0.0

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +206 -6
  3. data/bin/netlinx-workspace +27 -0
  4. data/doc/NetLinx.html +18 -7
  5. data/doc/NetLinx/Compile.html +15 -4
  6. data/doc/NetLinx/Compile/Extension.html +15 -4
  7. data/doc/NetLinx/Compile/Extension/APW.html +8 -8
  8. data/doc/NetLinx/Project.html +334 -90
  9. data/doc/NetLinx/Rake.html +128 -0
  10. data/doc/NetLinx/Rake/Workspace.html +128 -0
  11. data/doc/NetLinx/Rake/Workspace/CreateWorkspaceConfig.html +387 -0
  12. data/doc/NetLinx/Rake/Workspace/GenerateAPW.html +371 -0
  13. data/doc/NetLinx/System.html +1341 -217
  14. data/doc/NetLinx/SystemFile.html +454 -51
  15. data/doc/NetLinx/Workspace.html +434 -69
  16. data/doc/NetLinx/Workspace/YAML.html +398 -0
  17. data/doc/_index.html +66 -4
  18. data/doc/class_list.html +6 -2
  19. data/doc/file.README.html +218 -10
  20. data/doc/file.license.html +4 -4
  21. data/doc/file_list.html +5 -1
  22. data/doc/frames.html +1 -1
  23. data/doc/index.html +218 -10
  24. data/doc/js/full_list.js +4 -1
  25. data/doc/method_list.html +171 -23
  26. data/doc/top-level-namespace.html +3 -3
  27. data/lib/netlinx/compile/extension/apw.rb +3 -0
  28. data/lib/netlinx/rake/workspace.rb +14 -0
  29. data/lib/netlinx/rake/workspace/create_workspace_config.rb +49 -0
  30. data/lib/netlinx/rake/workspace/generate_apw.rb +41 -0
  31. data/lib/netlinx/workspace.rb +58 -14
  32. data/lib/netlinx/workspace/project.rb +107 -0
  33. data/lib/netlinx/workspace/system.rb +226 -0
  34. data/lib/netlinx/workspace/system_file.rb +98 -0
  35. data/lib/netlinx/workspace/yaml.rb +217 -0
  36. data/license.txt +1 -1
  37. metadata +53 -28
  38. data/lib/netlinx-workspace.rb +0 -1
  39. data/lib/netlinx/project.rb +0 -83
  40. data/lib/netlinx/system.rb +0 -122
  41. data/lib/netlinx/system_file.rb +0 -36
@@ -0,0 +1,41 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+
4
+ module NetLinx
5
+ module Rake
6
+ module Workspace
7
+
8
+ # Generate .apw workspace file from yaml config.
9
+ class GenerateAPW < ::Rake::TaskLib
10
+
11
+ attr_accessor :name
12
+
13
+ def initialize name = :generate_apw
14
+ @name = name
15
+ yield self if block_given?
16
+
17
+ desc "Generate .apw workspace file from yaml config."
18
+
19
+ task(name) do
20
+ require 'netlinx/workspace'
21
+
22
+ workspace_file = 'workspace.config.yaml'
23
+
24
+ unless File.exists? workspace_file
25
+ puts "File not found: #{workspace_file}"
26
+ next
27
+ end
28
+
29
+ NetLinx::Workspace::YAML.parse_file(workspace_file).tap do |workspace|
30
+ return unless workspace.name
31
+ File.open("#{workspace.name.strip}.apw", 'w') do |f|
32
+ f.write workspace.to_xml
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,8 +1,11 @@
1
+ # Gem convenience includes.
2
+ require_relative 'workspace/yaml'
3
+
4
+ # File includes.
1
5
  require 'rexml/document'
2
- require 'netlinx/project'
6
+ require_relative 'workspace/project'
3
7
 
4
8
  module NetLinx
5
-
6
9
  # A NetLinx Studio workspace.
7
10
  # Collection of projects.
8
11
  # Workspace -> Project -> System
@@ -26,17 +29,19 @@ module NetLinx
26
29
  nil
27
30
  end
28
31
 
29
- def initialize(**kvargs)
30
- @name = kvargs.fetch :name, ''
31
- @description = kvargs.fetch :description, ''
32
+ # @option kwargs [String] :name ('') Workspace name.
33
+ # @option kwargs [String] :description ('')
34
+ def initialize **kwargs
35
+ @name = kwargs.fetch :name, ''
36
+ @description = kwargs.fetch :description, ''
32
37
  @projects = []
33
38
 
34
- @file = kvargs.fetch :file, nil
39
+ @file = kwargs.fetch :file, nil
35
40
  load_workspace @file if @file
36
41
  end
37
42
 
38
43
  # Alias to add a project.
39
- def <<(project)
44
+ def << project
40
45
  @projects << project
41
46
  project.workspace = self
42
47
  end
@@ -51,8 +56,8 @@ module NetLinx
51
56
  File.dirname @file if @file
52
57
  end
53
58
 
54
- # Returns true if the workspace contains the specified file.
55
- def include?(file)
59
+ # @return true if the workspace contains the specified file.
60
+ def include? file
56
61
  included = false
57
62
 
58
63
  projects.each do |project|
@@ -74,10 +79,45 @@ module NetLinx
74
79
  compiler_results
75
80
  end
76
81
 
82
+ # @return [REXML::Element] an XML element representing this workspace.
83
+ def to_xml_element
84
+ REXML::Element.new('Workspace').tap do |workspace|
85
+ workspace.attributes['CurrentVersion'] = '4.0'
86
+
87
+ workspace.add_element('Identifier').tap { |e| e.text = name }
88
+ workspace.add_element('CreateVersion').tap { |e| e.text = '4.0' }
89
+ workspace.add_element('Comments').tap { |e| e.text = description }
90
+
91
+ @projects.each { |project| workspace << project.to_xml_element }
92
+ end
93
+ end
94
+
95
+ # @return [String] an XML string representing this workspace.
96
+ #
97
+ # @todo REXML bug forces :indent to be -1 or else erroneous line feeds are added.
98
+ # https://bugs.ruby-lang.org/issues/10864
99
+ def to_xml indent: -1
100
+ str = '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
101
+
102
+ REXML::Document.new.tap do |doc|
103
+ doc << to_xml_element
104
+ doc.write output: str, indent: indent
105
+ end
106
+
107
+ str + "\n"
108
+ end
109
+
110
+ # Generate a {NetLinx::Workspace} from an XML string.
111
+ # @return self
112
+ def parse_xml string
113
+ parse_xml_element REXML::Document.new(string)
114
+ self
115
+ end
116
+
77
117
  private
78
118
 
79
119
  # Load the workspace from a given NetLinx Studio .apw file.
80
- def load_workspace(file)
120
+ def load_workspace file
81
121
  raise LoadError, "File does not exist at:\n#{file}" unless File.exists? file
82
122
 
83
123
  doc = nil
@@ -85,12 +125,16 @@ module NetLinx
85
125
  doc = REXML::Document.new f
86
126
  end
87
127
 
128
+ parse_xml_element doc
129
+ end
130
+
131
+ def parse_xml_element root
88
132
  # Load workspace params.
89
- @name = doc.elements['/Workspace/Identifier'].text.strip
90
- @description = doc.elements['/Workspace/Comments'].text
91
-
133
+ @name = root.elements['/Workspace/Identifier'].text.strip || ''
134
+ @description = root.elements['/Workspace/Comments'].text || ''
135
+
92
136
  # Load projects.
93
- doc.each_element '/Workspace/Project' do |e|
137
+ root.each_element '/Workspace/Project' do |e|
94
138
  project = NetLinx::Project.new \
95
139
  element: e,
96
140
  workspace: self
@@ -0,0 +1,107 @@
1
+ require 'rexml/document'
2
+ require_relative 'system'
3
+
4
+ module NetLinx
5
+ # A collection of NetLinx systems.
6
+ # Workspace -> Project -> System
7
+ class Project
8
+ # A reference to the project's parent workspace.
9
+ attr_accessor :workspace
10
+ attr_accessor :name
11
+ attr_accessor :description
12
+ attr_accessor :dealer
13
+ attr_accessor :designer
14
+ attr_accessor :sales_order
15
+ attr_accessor :purchase_order
16
+ attr_accessor :systems
17
+
18
+ # @option kwargs [NetLinx::Workspace] :workspace This system's parent workspace node.
19
+ # @option kwargs [String] :name ('') Project name.
20
+ # @option kwargs [String] :description ('')
21
+ # @option kwargs [String] :dealer ('')
22
+ # @option kwargs [String] :designer ('')
23
+ # @option kwargs [String] :sales_order ('')
24
+ # @option kwargs [String] :purchase_order ('')
25
+ def initialize **kwargs
26
+ @workspace = kwargs.fetch :workspace, nil
27
+
28
+ @name = kwargs.fetch :name, ''
29
+ @description = kwargs.fetch :description, ''
30
+ @dealer = kwargs.fetch :dealer, ''
31
+ @designer = kwargs.fetch :designer, ''
32
+ @sales_order = kwargs.fetch :sales_order, ''
33
+ @purchase_order = kwargs.fetch :purchase_order, ''
34
+
35
+ @systems = []
36
+
37
+ project_element = kwargs.fetch :element, nil
38
+ parse_xml_element project_element if project_element
39
+ end
40
+
41
+ # Alias to add a system.
42
+ def << system
43
+ @systems << system
44
+ system.project = self
45
+ end
46
+
47
+ # @return the project name.
48
+ def to_s
49
+ @name
50
+ end
51
+
52
+ # @return true if the project contains the specified file.
53
+ def include? file
54
+ included = false
55
+
56
+ systems.each do |system|
57
+ included = system.include? file
58
+ break if included
59
+ end
60
+
61
+ included
62
+ end
63
+
64
+ # Compile all systems in this project.
65
+ def compile
66
+ compiler_results = []
67
+ @systems.each {|system| compiler_results << (system.compile).first}
68
+ compiler_results
69
+ end
70
+
71
+ # @return [REXML::Element] an XML element representing this project.
72
+ def to_xml_element
73
+ REXML::Element.new('Project').tap do |project|
74
+ project.add_element('Identifier').tap { |e| e.text = name }
75
+ project.add_element('Designer').tap { |e| e.text = designer }
76
+ project.add_element('DealerID').tap { |e| e.text = dealer }
77
+ project.add_element('SalesOrder').tap { |e| e.text = sales_order }
78
+ project.add_element('PurchaseOrder').tap { |e| e.text = purchase_order }
79
+ project.add_element('Comments').tap { |e| e.text = description }
80
+
81
+ @systems.each { |system| project << system.to_xml_element }
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def parse_xml_element project
88
+ # Load project params.
89
+ @name = project.elements['Identifier'].text.strip || ''
90
+ @designer = project.elements['Designer'].text || ''
91
+ @dealer = project.elements['DealerID'].text || ''
92
+ @sales_order = project.elements['SalesOrder'].text || ''
93
+ @purchase_order = project.elements['PurchaseOrder'].text || ''
94
+ @description = project.elements['Comments'].text || ''
95
+
96
+ # Load systems.
97
+ project.each_element 'System' do |e|
98
+ system = NetLinx::System.new \
99
+ element: e,
100
+ project: self
101
+
102
+ @systems << system
103
+ end
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,226 @@
1
+ require 'rexml/document'
2
+ require_relative 'system_file'
3
+
4
+ module NetLinx
5
+ # A collection of resources loaded onto a NetLinx master.
6
+ # Workspace -> Project -> System
7
+ class System
8
+ # A reference to the system's parent project.
9
+ attr_accessor :project
10
+ attr_accessor :files
11
+
12
+ attr_accessor :name
13
+ attr_accessor :active
14
+ attr_accessor :id
15
+ attr_accessor :description
16
+
17
+ attr_accessor :ip_address
18
+ attr_accessor :ip_port
19
+ attr_accessor :ensure_availability
20
+
21
+ attr_accessor :com_port
22
+ attr_accessor :baud_rate
23
+ attr_accessor :data_bits
24
+ attr_accessor :parity
25
+ attr_accessor :stop_bits
26
+ attr_accessor :flow_control
27
+
28
+ # @option kwargs [NetLinx::Project] :project This system's parent project node.
29
+ # @option kwargs [String] :name ('') System name.
30
+ # @option kwargs [String] :description ('')
31
+ # @option kwargs [Boolean] :active (false) True if this is the active system
32
+ # in the workspace.
33
+ # @option kwargs [Integer] :id (0) Master controller system ID.
34
+ # 0 connects to any master at the given communication settings.
35
+ # Or in other words, 0 prevents disconnection from a master
36
+ # with a different ID.
37
+ #
38
+ # @option kwargs [String] :ip_address ('0.0.0.0')
39
+ # @option kwargs [String] :ip_port (1319) ICSLan port.
40
+ # @option kwargs [String] :ensure_availability (true) Ping the master
41
+ # controller to ensure availability before connecting.
42
+ #
43
+ # @option kwargs [Symbol] :com_port (:com1)
44
+ # @option kwargs [Integer] :baud_rate (38400)
45
+ # @option kwargs [Integer] :data_bits (8)
46
+ # @option kwargs [:none,:even,:odd,:mark,:space] :parity (:none)
47
+ # @option kwargs [Integer] :stop_bits (1)
48
+ # @option kwargs [:none] :flow_control (:none)
49
+ def initialize **kwargs
50
+ @project = kwargs.fetch :project, nil
51
+
52
+ @name = kwargs.fetch :name, ''
53
+ @id = kwargs.fetch :id, 0
54
+ @active = kwargs.fetch :active, false
55
+ @description = kwargs.fetch :description, ''
56
+
57
+ @ip_address = kwargs.fetch :ip_address, '0.0.0.0'
58
+ @ip_port = kwargs.fetch :ip_port, 1319
59
+ @ensure_availability = kwargs.fetch :ensure_availability, true
60
+
61
+ @com_port = kwargs.fetch :com_port, :com1
62
+ @baud_rate = kwargs.fetch :baud_rate, 38400
63
+ @data_bits = kwargs.fetch :data_bits, 8
64
+ @parity = kwargs.fetch :parity, :none
65
+ @stop_bits = kwargs.fetch :stop_bits, 1
66
+ @flow_control = kwargs.fetch :flow_control, :none
67
+
68
+ @files = []
69
+
70
+ @compiler_target_files = []
71
+ @compiler_include_paths = []
72
+ @compiler_module_paths = []
73
+ @compiler_library_paths = []
74
+
75
+ system_element = kwargs.fetch :element, nil
76
+ parse_xml_element system_element if system_element
77
+ end
78
+
79
+ # Alias to add a file.
80
+ def << file
81
+ @files << file
82
+ file.system = self
83
+ end
84
+
85
+ # @return the system name.
86
+ def to_s
87
+ @name
88
+ end
89
+
90
+ # @return [REXML::Element] an XML element representing this system.
91
+ def to_xml_element
92
+ REXML::Element.new('System').tap do |system|
93
+ system.attributes['IsActive'] = active
94
+ system.attributes['Platform'] = 'Netlinx'
95
+ system.attributes['Transport'] = 'Serial'
96
+ system.attributes['TransportEx'] =
97
+ (ip_address == '0.0.0.0') ? 'Serial' : 'TCPIP'
98
+
99
+ system.add_element('Identifier').tap { |e| e.text = name }
100
+ system.add_element('SysID').tap { |e| e.text = id }
101
+ system.add_element('Comments').tap { |e| e.text = description }
102
+
103
+ # These don't seem to change in NetLinx Studio 4.0; possibly 3.x legacy.
104
+ # The 'Ex' suffixes are used.
105
+ system.add_element('TransTCPIP').tap { |e| e.text = '0.0.0.0' }
106
+ system.add_element('TransSerial').tap { |e| e.text = 'COM1,38400,8,None,1,None' }
107
+
108
+ # TODO: Generate communication settings.
109
+ system.add_element('TransTCPIPEx').tap { |e|
110
+ e.text = "#{ip_address}|#{ip_port}|#{ensure_availability ? 1 : 0}|||"
111
+ }
112
+ system.add_element('TransSerialEx').tap { |e|
113
+ e.text = "#{com_port.upcase}|#{baud_rate}|#{data_bits}|#{parity.capitalize}|#{stop_bits}|||"
114
+ }
115
+ system.add_element('TransUSBEx').tap { |e| e.text = '|||||' }
116
+ system.add_element('TransVNMEx').tap { |e| e.text = '||' }
117
+
118
+ system.add_element('UserName').tap { |e| e.text = '' }
119
+ system.add_element('Password').tap { |e| e.text = '' }
120
+
121
+ @files.each { |file| system << file.to_xml_element }
122
+ end
123
+ end
124
+
125
+ # @see Test::NetLinx::Compilable.
126
+ def compiler_target_files
127
+ @files
128
+ .select {|f| f.type == :master}
129
+ .map {|f| File.expand_path \
130
+ f.path.gsub('\\', '/'),
131
+ f.system.project.workspace.path
132
+ }.uniq
133
+ end
134
+
135
+ # @see Test::NetLinx::Compilable.
136
+ def compiler_include_paths
137
+ @files
138
+ .select {|f| f.type == :include}
139
+ .map {|f| File.expand_path \
140
+ File.dirname(f.path.gsub('\\', '/')),
141
+ f.system.project.workspace.path
142
+ }.uniq
143
+ end
144
+
145
+ # @see Test::NetLinx::Compilable.
146
+ def compiler_module_paths
147
+ @files
148
+ .select {|f| f.type == :module || f.type == :tko || f.type == :duet}
149
+ .map {|f| File.expand_path \
150
+ File.dirname(f.path.gsub('\\', '/')),
151
+ f.system.project.workspace.path
152
+ }.uniq
153
+ end
154
+
155
+ # @see Test::NetLinx::Compilable.
156
+ def compiler_library_paths
157
+ []
158
+ end
159
+
160
+ # @return [Boolean] true if the project contains the specified file.
161
+ def include? file
162
+ included = false
163
+
164
+ @files.each do |f|
165
+ name_included = f.name.downcase.eql? file.downcase
166
+
167
+ # TODO: This should probably be relative to the workspace path,
168
+ # which can be found by traversing @project, @workspace.
169
+ path_included = file.gsub(/\\/, '/').include? f.path.gsub(/\\/, '/')
170
+
171
+ included = name_included || path_included
172
+ break if included
173
+ end
174
+
175
+ included
176
+ end
177
+
178
+ # Compile this system.
179
+ def compile
180
+ # The compiler dependency is only needed if this method is called.
181
+ require 'netlinx/compiler'
182
+
183
+ compiler = NetLinx::Compiler.new
184
+ compiler.compile self
185
+ end
186
+
187
+ private
188
+
189
+ def parse_xml_element system
190
+ # Load system params.
191
+ @name = system.elements['Identifier'].text.strip || ''
192
+ @active = (system.attributes['IsActive'].strip == 'true') ? true : false
193
+ @id = system.elements['SysID'].text.strip.to_i || 0
194
+ @description = system.elements['Comments'].text || ''
195
+
196
+ if system.elements['TransTCPIPEx'] # Workspace v4.0
197
+ tcpip = (system.elements['TransTCPIPEx'].text || '').split('|')
198
+
199
+ @ip_address = tcpip[0] || '0.0.0.0'
200
+ @ip_port = (tcpip[1] || 1319).to_i
201
+ @ensure_availability = (tcpip[2] == '0') ? false : true
202
+ end
203
+
204
+ if system.elements['TransSerialEx'] # Workspace v4.0
205
+ serial = (system.elements['TransSerialEx'].text || '').split('|')
206
+
207
+ @com_port = (serial[0] || :com1).to_s.downcase.to_sym
208
+ @baud_rate = (serial[1] || 38400).to_i
209
+ @data_bits = (serial[2] || 8).to_i
210
+ @parity = (serial[3] || :none).to_s.downcase.to_sym
211
+ @stop_bits = (serial[4] || 1).to_i
212
+ @flow_control = (serial[5] || :none).to_s.downcase.to_sym
213
+ end
214
+
215
+ # Create system files.
216
+ system.each_element 'File' do |e|
217
+ system_file = NetLinx::SystemFile.new \
218
+ element: e,
219
+ system: self
220
+
221
+ @files << system_file
222
+ end
223
+ end
224
+
225
+ end
226
+ end