netlinx-workspace 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +206 -6
- data/bin/netlinx-workspace +27 -0
- data/doc/NetLinx.html +18 -7
- data/doc/NetLinx/Compile.html +15 -4
- data/doc/NetLinx/Compile/Extension.html +15 -4
- data/doc/NetLinx/Compile/Extension/APW.html +8 -8
- data/doc/NetLinx/Project.html +334 -90
- data/doc/NetLinx/Rake.html +128 -0
- data/doc/NetLinx/Rake/Workspace.html +128 -0
- data/doc/NetLinx/Rake/Workspace/CreateWorkspaceConfig.html +387 -0
- data/doc/NetLinx/Rake/Workspace/GenerateAPW.html +371 -0
- data/doc/NetLinx/System.html +1341 -217
- data/doc/NetLinx/SystemFile.html +454 -51
- data/doc/NetLinx/Workspace.html +434 -69
- data/doc/NetLinx/Workspace/YAML.html +398 -0
- data/doc/_index.html +66 -4
- data/doc/class_list.html +6 -2
- data/doc/file.README.html +218 -10
- data/doc/file.license.html +4 -4
- data/doc/file_list.html +5 -1
- data/doc/frames.html +1 -1
- data/doc/index.html +218 -10
- data/doc/js/full_list.js +4 -1
- data/doc/method_list.html +171 -23
- data/doc/top-level-namespace.html +3 -3
- data/lib/netlinx/compile/extension/apw.rb +3 -0
- data/lib/netlinx/rake/workspace.rb +14 -0
- data/lib/netlinx/rake/workspace/create_workspace_config.rb +49 -0
- data/lib/netlinx/rake/workspace/generate_apw.rb +41 -0
- data/lib/netlinx/workspace.rb +58 -14
- data/lib/netlinx/workspace/project.rb +107 -0
- data/lib/netlinx/workspace/system.rb +226 -0
- data/lib/netlinx/workspace/system_file.rb +98 -0
- data/lib/netlinx/workspace/yaml.rb +217 -0
- data/license.txt +1 -1
- metadata +53 -28
- data/lib/netlinx-workspace.rb +0 -1
- data/lib/netlinx/project.rb +0 -83
- data/lib/netlinx/system.rb +0 -122
- 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
|
data/lib/netlinx/workspace.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# Gem convenience includes.
|
2
|
+
require_relative 'workspace/yaml'
|
3
|
+
|
4
|
+
# File includes.
|
1
5
|
require 'rexml/document'
|
2
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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 =
|
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 <<
|
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
|
-
#
|
55
|
-
def include?
|
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
|
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 =
|
90
|
-
@description =
|
91
|
-
|
133
|
+
@name = root.elements['/Workspace/Identifier'].text.strip || ''
|
134
|
+
@description = root.elements['/Workspace/Comments'].text || ''
|
135
|
+
|
92
136
|
# Load projects.
|
93
|
-
|
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
|