rsmp 0.1.13 → 0.1.29

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/lib/rsmp/cli.rb CHANGED
@@ -2,106 +2,137 @@ require 'thor'
2
2
  require 'rsmp'
3
3
 
4
4
  module RSMP
5
- class CLI < Thor
6
-
7
- desc "site", "Run RSMP site"
8
- method_option :config, :type => :string, :aliases => "-c", banner: 'Path to .yaml config file'
9
- method_option :id, :type => :string, :aliases => "-i", banner: 'RSMP site id'
10
- method_option :supervisors, :type => :string, :aliases => "-s", banner: 'ip:port,... list of supervisor to connect to'
11
- method_option :log, :type => :string, :aliases => "-l", banner: 'Path to log file'
12
- method_option :json, :type => :boolean, :aliases => "-j", banner: 'Show JSON messages in log'
13
- method_option :type, :type => :string, :aliases => "-t", banner: 'Type of site: [tlc]'
14
- def site
15
- settings = {}
16
- log_settings = { 'active' => true }
17
-
18
- if options[:config]
19
- if File.exist? options[:config]
20
- settings = YAML.load_file options[:config]
21
- log_settings = settings.delete 'log'
22
- else
23
- puts "Error: Config #{options[:config]} not found"
24
- exit
25
- end
26
- end
27
-
28
- if options[:id]
29
- settings['site_id'] = options[:id]
30
- end
31
-
32
- if options[:supervisors]
33
- options[:supervisors].split(',').each do |supervisor|
34
- settings[:supervisors] ||= []
35
- ip, port = supervisor.split ':'
36
- ip = '127.0.0.1' if ip.empty?
37
- port = '12111' if port.empty?
38
- settings[:supervisors] << {"ip"=>ip, "port"=>port}
39
- end
40
- end
41
-
42
- if options[:log]
43
- log_settings['path'] = options[:log]
44
- end
45
-
46
- if options[:json]
47
- log_settings['json'] = options[:json]
48
- end
49
-
50
- site_class = RSMP::Site
51
- if options[:type]
52
- case options[:type]
53
- when 'tlc'
54
- site_class = RSMP::Tlc
55
- else
56
- site_class = RSMP::Site
57
- end
58
- end
59
- site_class.new(site_settings:settings, log_settings: log_settings).start
60
- end
61
-
62
- desc "supervisor", "Run RSMP supervisor"
63
- method_option :config, :type => :string, :aliases => "-c", banner: 'Path to .yaml config file'
64
- method_option :id, :type => :string, :aliases => "-i", banner: 'RSMP site id'
65
- method_option :ip, :type => :numeric, banner: 'IP address to listen on'
66
- method_option :port, :type => :string, :aliases => "-p", banner: 'Port to listen on'
67
- method_option :log, :type => :string, :aliases => "-l", banner: 'Path to log file'
68
- method_option :json, :type => :boolean, :aliases => "-j", banner: 'Show JSON messages in log'
69
- def supervisor
70
- settings = {}
71
- log_settings = { 'active' => true }
72
-
73
- if options[:config]
74
- if File.exist? options[:config]
75
- settings = YAML.load_file options[:config]
76
- log_settings = settings.delete 'log'
77
- else
78
- puts "Error: Config #{options[:config]} not found"
79
- exit
80
- end
81
- end
82
-
83
- if options[:id]
84
- settings['site_id'] = options[:id]
85
- end
86
-
87
- if options[:ip]
88
- settings['ip'] = options[:ip]
89
- end
90
-
91
- if options[:port]
92
- settings['port'] = options[:port]
93
- end
94
-
95
- if options[:log]
96
- log_settings['path'] = options[:log]
97
- end
98
-
99
- if options[:json]
100
- log_settings['json'] = options[:json]
101
- end
102
-
103
- RSMP::Supervisor.new(supervisor_settings:settings,log_settings:log_settings).start
104
- end
105
-
106
- end
5
+ class CLI < Thor
6
+
7
+ desc "site", "Run RSMP site"
8
+ method_option :config, :type => :string, :aliases => "-c", banner: 'Path to .yaml config file'
9
+ method_option :id, :type => :string, :aliases => "-i", banner: 'RSMP site id'
10
+ method_option :supervisors, :type => :string, :aliases => "-s", banner: 'ip:port,... list of supervisor to connect to'
11
+ method_option :log, :type => :string, :aliases => "-l", banner: 'Path to log file'
12
+ method_option :json, :type => :boolean, :aliases => "-j", banner: 'Show JSON messages in log'
13
+ method_option :type, :type => :string, :aliases => "-t", banner: 'Type of site: [tlc]'
14
+ def site
15
+ settings = {}
16
+ log_settings = { 'active' => true }
17
+
18
+ if options[:config]
19
+ if File.exist? options[:config]
20
+ settings = YAML.load_file options[:config]
21
+ log_settings = settings.delete('log') || {}
22
+ else
23
+ puts "Error: Config #{options[:config]} not found"
24
+ exit
25
+ end
26
+ end
27
+
28
+ if options[:id]
29
+ settings['site_id'] = options[:id]
30
+ end
31
+
32
+ if options[:supervisors]
33
+ options[:supervisors].split(',').each do |supervisor|
34
+ settings['supervisors'] ||= []
35
+ ip, port = supervisor.split ':'
36
+ ip = '127.0.0.1' if ip.empty?
37
+ port = '12111' if port.empty?
38
+ settings['supervisors'] << {"ip"=>ip, "port"=>port}
39
+ end
40
+ end
41
+
42
+ if options[:log]
43
+ log_settings['path'] = options[:log]
44
+ end
45
+
46
+ if options[:json]
47
+ log_settings['json'] = options[:json]
48
+ end
49
+
50
+ site_class = RSMP::Site
51
+ if options[:type]
52
+ case options[:type]
53
+ when 'tlc'
54
+ site_class = RSMP::Tlc
55
+ else
56
+ site_class = RSMP::Site
57
+ end
58
+ end
59
+ site_class.new(site_settings:settings, log_settings: log_settings).start
60
+ rescue RSMP::Schemer::UnknownSchemaTypeError => e
61
+ puts "Cannot start site: #{e}"
62
+ rescue RSMP::Schemer::UnknownSchemaVersionError => e
63
+ puts "Cannot start site: #{e}"
64
+ rescue Psych::SyntaxError => e
65
+ puts "Cannot read config file #{e}"
66
+ end
67
+
68
+ desc "supervisor", "Run RSMP supervisor"
69
+ method_option :config, :type => :string, :aliases => "-c", banner: 'Path to .yaml config file'
70
+ method_option :id, :type => :string, :aliases => "-i", banner: 'RSMP site id'
71
+ method_option :ip, :type => :numeric, banner: 'IP address to listen on'
72
+ method_option :port, :type => :string, :aliases => "-p", banner: 'Port to listen on'
73
+ method_option :log, :type => :string, :aliases => "-l", banner: 'Path to log file'
74
+ method_option :json, :type => :boolean, :aliases => "-j", banner: 'Show JSON messages in log'
75
+ def supervisor
76
+ settings = {}
77
+ log_settings = { 'active' => true }
78
+
79
+ if options[:config]
80
+ if File.exist? options[:config]
81
+ settings = YAML.load_file options[:config]
82
+ log_settings = settings.delete 'log'
83
+ else
84
+ puts "Error: Config #{options[:config]} not found"
85
+ exit
86
+ end
87
+ end
88
+
89
+ if options[:id]
90
+ settings['site_id'] = options[:id]
91
+ end
92
+
93
+ if options[:ip]
94
+ settings['ip'] = options[:ip]
95
+ end
96
+
97
+ if options[:port]
98
+ settings['port'] = options[:port]
99
+ end
100
+
101
+ if options[:log]
102
+ log_settings['path'] = options[:log]
103
+ end
104
+
105
+ if options[:json]
106
+ log_settings['json'] = options[:json]
107
+ end
108
+
109
+ RSMP::Supervisor.new(supervisor_settings:settings,log_settings:log_settings).start
110
+ rescue RSMP::ConfigurationError => e
111
+ puts "Cannot start supervisor: #{e}"
112
+ end
113
+
114
+ desc "convert", "Convert SXL from YAML to JSON Schema"
115
+ method_option :in, :type => :string, :aliases => "-i", banner: 'Path to YAML input file'
116
+ method_option :out, :type => :string, :aliases => "-o", banner: 'Path to JSON Schema output file'
117
+ def convert
118
+ unless options[:in]
119
+ puts "Error: Input option missing"
120
+ exit
121
+ end
122
+
123
+ unless options[:out]
124
+ puts "Error: Output option missing"
125
+ exit
126
+ end
127
+
128
+ unless File.exist? options[:in]
129
+ puts "Error: Input path file #{options[:in]} not found"
130
+ exit
131
+ end
132
+
133
+ sxl = RSMP::Convert::Import::YAML.read options[:in]
134
+ RSMP::Convert::Export::JSONSchema.write sxl, options[:out]
135
+ end
136
+
137
+ end
107
138
  end
@@ -0,0 +1,102 @@
1
+ # Collects matching ingoing and/or outgoing messages and
2
+ # wakes up the client once the desired amount has been collected.
3
+ # Can listen for ingoing and/or outgoing messages.
4
+
5
+ module RSMP
6
+ class Collector < Listener
7
+
8
+ attr_reader :condition, :messages, :done
9
+
10
+ def initialize proxy, options={}
11
+ super proxy, options
12
+ @ingoing = options[:ingoing] == nil ? true : options[:ingoing]
13
+ @outgoing = options[:outgoing] == nil ? false : options[:outgoing]
14
+ @messages = []
15
+ @condition = Async::Notification.new
16
+ @done = false
17
+ @options = options
18
+ @num = options[:num]
19
+ end
20
+
21
+ def inspect
22
+ "#<#{self.class.name}:#{self.object_id}, #{inspector(:@messages)}>"
23
+ end
24
+
25
+ def ingoing?
26
+ @ingoing == true
27
+ end
28
+
29
+ def outgoing?
30
+ @outgoing == true
31
+ end
32
+
33
+ def wait
34
+ @condition.wait
35
+ end
36
+
37
+ def collect_for task, duration
38
+ siphon do
39
+ task.sleep duration
40
+ end
41
+ end
42
+
43
+ def collect task, options={}, &block
44
+ @num = options[:num] if options[:num]
45
+ @options[:timeout] = options[:timeout] if options[:timeout]
46
+ @block = block
47
+
48
+ listen do
49
+ task.with_timeout(@options[:timeout]) do
50
+ @condition.wait
51
+ end
52
+ end
53
+
54
+ if @num == 1
55
+ @messages = @messages.first # if one message was requested, return it instead of array
56
+ else
57
+ @messages = @messages.first @num # return array, but ensure we never return more than requested
58
+ end
59
+ @messages
60
+ end
61
+
62
+ def reset
63
+ @message.clear
64
+ @done = false
65
+ end
66
+
67
+ def notify message
68
+ raise ArgumentError unless message
69
+ return true if @done
70
+ return if message.direction == :in && @ingoing == false
71
+ return if message.direction == :out && @outgoing == false
72
+ if matches? message
73
+ @messages << message
74
+ if @num && @messages.size >= @num
75
+ @done = true
76
+ @proxy.remove_listener self
77
+ @condition.signal
78
+ end
79
+ end
80
+ end
81
+
82
+ def matches? message
83
+ raise ArgumentError unless message
84
+
85
+ if @options[:type]
86
+ return false if message == nil
87
+ if @options[:type].is_a? Array
88
+ return false unless @options[:type].include? message.type
89
+ else
90
+ return false unless message.type == @options[:type]
91
+ end
92
+ end
93
+ if @options[:component]
94
+ return false if message.attributes['cId'] && message.attributes['cId'] != @options[:component]
95
+ end
96
+ if @block
97
+ return false if @block.call(message) == false
98
+ end
99
+ true
100
+ end
101
+ end
102
+ end
@@ -1,6 +1,8 @@
1
1
  module RSMP
2
2
  class Component
3
- attr_reader :c_id, :alarms, :statuses, :aggregated_status, :aggregated_status_bools, :grouped
3
+ include Inspect
4
+
5
+ attr_reader :c_id, :node, :alarms, :statuses, :aggregated_status, :aggregated_status_bools, :grouped
4
6
 
5
7
  AGGREGATED_STATUS_KEYS = [ :local_control,
6
8
  :communication_distruption,
@@ -23,6 +25,7 @@ module RSMP
23
25
  def clear_aggregated_status
24
26
  @aggregated_status = []
25
27
  @aggregated_status_bools = Array.new(8,false)
28
+ @aggregated_status_bools[5] = true
26
29
  end
27
30
 
28
31
  def set_aggregated_status status
@@ -58,11 +61,17 @@ module RSMP
58
61
  def alarm code:, status:
59
62
  end
60
63
 
61
- def status code:, value:
62
- end
63
-
64
64
  def log str, options
65
65
  @node.log str, options
66
66
  end
67
+
68
+ def handle_command command_code, arg
69
+ raise UnknownCommand.new "Command #{command_code} not implemented by #{self.class}"
70
+ end
71
+
72
+ def get_status status_code, status_name=nil
73
+ raise UnknownStatus.new "Status #{status_code}/#{status_name} not implemented by #{self.class}"
74
+ end
75
+
67
76
  end
68
77
  end
@@ -1,10 +1,10 @@
1
1
  # Things shared between sites and site proxies
2
2
 
3
3
  module RSMP
4
- module SiteBase
4
+ module Components
5
5
  attr_reader :components
6
6
 
7
- def initialize_site
7
+ def initialize_components
8
8
  @components = {}
9
9
  end
10
10
 
@@ -13,8 +13,10 @@ module RSMP
13
13
 
14
14
  def setup_components settings
15
15
  return unless settings
16
- settings.each_pair do |id,settings|
17
- @components[id] = build_component(id,settings)
16
+ settings.each_pair do |type,components_by_type|
17
+ components_by_type.each_pair do |id,settings|
18
+ @components[id] = build_component(id:id, type:type, settings:settings)
19
+ end
18
20
  end
19
21
  end
20
22
 
@@ -22,8 +24,8 @@ module RSMP
22
24
  @components[component.c_id] = component
23
25
  end
24
26
 
25
- def build_component id, settings={}
26
- Component.new id: id, node: self, grouped: true
27
+ def build_component id:, type:, settings:{}
28
+ Component.new id:id, node: self, grouped: type=='main'
27
29
  end
28
30
 
29
31
  def find_component component_id
@@ -0,0 +1,204 @@
1
+ # Import SXL from YAML format
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'fileutils'
6
+
7
+ module RSMP
8
+ module Convert
9
+ module Export
10
+ module JSONSchema
11
+
12
+ @@json_options = {
13
+ array_nl: "\n",
14
+ object_nl: "\n",
15
+ indent: ' ',
16
+ space_before: ' ',
17
+ space: ' '
18
+ }
19
+
20
+ def self.output_json item
21
+ JSON.generate(item,@@json_options)
22
+ end
23
+
24
+ def self.build_value item
25
+ out = {}
26
+
27
+ if item['description']
28
+ out["description"] = item['description']
29
+ end
30
+
31
+ if item['list']
32
+ case item['type']
33
+ when "boolean"
34
+ out["$ref"] = "../../core/definitions.json#/boolean_list"
35
+ when "integer", "ordinal", "unit", "scale", "long"
36
+ out["$ref"] = "../../core/definitions.json#/integer_list"
37
+ else
38
+ raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
39
+ end
40
+
41
+ if item["values"]
42
+ value_list = item["values"].keys.join('|')
43
+ out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
44
+ end
45
+
46
+ if item["pattern"]
47
+ puts "Warning: Pattern not support for lists: #{item.inspect}"
48
+ end
49
+ else
50
+ case item['type']
51
+ when "string", "base64"
52
+ out["type"] = "string"
53
+ when "boolean"
54
+ out["$ref"] = "../../core/definitions.json#/boolean"
55
+ when "timestamp"
56
+ out["$ref"] = "../../core/definitions.json#/timestamp"
57
+ when "integer", "ordinal", "unit", "scale", "long"
58
+ out["$ref"] = "../../core/definitions.json#/integer"
59
+ else
60
+ out["type"] = "string"
61
+ end
62
+
63
+ if item["values"]
64
+ out["enum"] = item["values"].keys.sort
65
+ end
66
+
67
+ if item["pattern"]
68
+ out["pattern"] = item["pattern"]
69
+ end
70
+ end
71
+
72
+
73
+ out
74
+ end
75
+
76
+ def self.build_item item, property_key: 'v'
77
+ json = { "allOf" => [ { "description" => item['description'] } ] }
78
+ if item['arguments']
79
+ json["allOf"].first["properties"] = { "n" => { "enum" => item['arguments'].keys.sort } }
80
+ item['arguments'].each_pair do |key,argument|
81
+ json["allOf"] << {
82
+ "if" => { "required" => ["n"], "properties" => { "n" => { "const" => key }}},
83
+ "then" => { "properties" => { property_key => build_value(argument) } }
84
+ }
85
+ end
86
+ end
87
+ json
88
+ end
89
+
90
+ def self.output_alarms out, items
91
+ list = items.keys.sort.map do |key|
92
+ {
93
+ "if" => { "required" => ["aCId"], "properties" => { "aCId" => { "const" => key }}},
94
+ "then" => { "$ref" => "#{key}.json" }
95
+ }
96
+ end
97
+ json = {
98
+ "properties" => {
99
+ "aCId" => { "enum" => items.keys.sort },
100
+ "rvs" => { "items" => { "allOf" => list } }
101
+ }
102
+ }
103
+ out['alarms/alarms.json'] = output_json json
104
+ items.each_pair { |key,item| output_alarm out, key, item }
105
+ end
106
+
107
+ def self.output_alarm out, key, item
108
+ json = build_item item
109
+ out["alarms/#{key}.json"] = output_json json
110
+ end
111
+
112
+ def self.output_statuses out, items
113
+ list = [ { "properties" => { "sCI" => { "enum"=> items.keys.sort }}} ]
114
+ items.keys.sort.each do |key|
115
+ list << {
116
+ "if"=> { "required" => ["sCI"], "properties" => { "sCI"=> { "const"=> key }}},
117
+ "then" => { "$ref" => "#{key}.json" }
118
+ }
119
+ end
120
+ json = { "properties" => { "sS" => { "items" => { "allOf" => list }}}}
121
+ out['statuses/statuses.json'] = output_json json
122
+ items.each_pair { |key,item| output_status out, key, item }
123
+ end
124
+
125
+ def self.output_status out, key, item
126
+ json = build_item item, property_key:'s'
127
+ out["statuses/#{key}.json"] = output_json json
128
+ end
129
+
130
+ def self.output_commands out, items
131
+ list = [ { "properties" => { "cCI" => { "enum"=> items.keys.sort }}} ]
132
+ items.keys.sort.each do |key|
133
+ list << {
134
+ "if" => { "required" => ["cCI"], "properties" => { "cCI"=> { "const"=> key }}},
135
+ "then" => { "$ref" => "#{key}.json" }
136
+ }
137
+ end
138
+ json = { "items" => { "allOf" => list }}
139
+ out['commands/commands.json'] = output_json json
140
+
141
+ json = { "properties" => { "arg" => { "$ref" => "commands.json" }}}
142
+ out['commands/command_requests.json'] = output_json json
143
+
144
+ json = { "properties" => { "rvs" => { "$ref" => "commands.json" }}}
145
+ out['commands/command_responses.json'] = output_json json
146
+
147
+ items.each_pair { |key,item| output_command out, key, item }
148
+ end
149
+
150
+ def self.output_command out, key, item
151
+ json = build_item item
152
+ json["allOf"].first["properties"]['cO'] = { "const" => item['command'] }
153
+ out["commands/#{key}.json"] = output_json json
154
+ end
155
+
156
+ def self.output_root out
157
+ json = {
158
+ "description"=> "A schema validatating message against the RSMP SXL for Traffic Light Controllers",
159
+ "allOf" => [
160
+ {
161
+ "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "CommandRequest" }}},
162
+ "then" => { "$ref" => "commands/command_requests.json" }
163
+ },
164
+ {
165
+ "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "CommandResponse" }}},
166
+ "then" => { "$ref" => "commands/command_responses.json" }
167
+ },
168
+ {
169
+ "if" => { "required" => ["type"], "properties" => { "type" => { "enum" => ["StatusRequest","StatusResponse","StatusSubscribe","StatusUnsubscribe","StatusUpdate"] }}},
170
+ "then" => { "$ref" => "statuses/statuses.json" }
171
+ },
172
+ {
173
+ "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "Alarm" }}},
174
+ "then" => { "$ref" => "alarms/alarms.json" }
175
+ }
176
+ ]
177
+ }
178
+ out["sxl.json"] = output_json json
179
+ end
180
+
181
+ def self.generate sxl
182
+ out = {}
183
+ output_root out
184
+ output_alarms out, sxl[:alarms]
185
+ output_statuses out, sxl[:statuses]
186
+ output_commands out, sxl[:commands]
187
+ out
188
+ end
189
+
190
+ def self.write sxl, folder
191
+ out = generate sxl
192
+ out.each_pair do |relative_path,str|
193
+ path = File.join(folder, relative_path)
194
+ FileUtils.mkdir_p File.dirname(path) # create folders if needed
195
+ file = File.open(path, 'w+') # w+ means truncate or create new file
196
+ file.puts str
197
+ end
198
+ end
199
+
200
+ end
201
+
202
+ end
203
+ end
204
+ end