rsmp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ module RSMP
2
+ class Component
3
+ attr_reader :c_id, :alarms, :statuses, :aggregated_status, :aggregated_status_bools, :grouped
4
+
5
+ AGGREGATED_STATUS_KEYS = [ :local_control,
6
+ :communication_distruption,
7
+ :high_priority_alarm,
8
+ :medium_priority_alarm,
9
+ :low_priority_alarm,
10
+ :normal,
11
+ :rest,
12
+ :not_connected ]
13
+
14
+ def initialize node:, id:, grouped:
15
+ @c_id = id
16
+ @node = node
17
+ @grouped = grouped
18
+ @alarms = {}
19
+ @statuses = {}
20
+ clear_aggregated_status
21
+ end
22
+
23
+ def clear_aggregated_status
24
+ @aggregated_status = []
25
+ @aggregated_status_bools = Array.new(8,false)
26
+ end
27
+
28
+ def set_aggregated_status status
29
+ status = [status] if status.is_a? Symbol
30
+ raise InvalidArgument unless status.is_a? Array
31
+ input = status & AGGREGATED_STATUS_KEYS
32
+ if input != @aggregated_status
33
+ AGGREGATED_STATUS_KEYS.each_with_index do |key,index|
34
+ @aggregated_status_bools[index] = status.include?(key)
35
+ end
36
+ aggrated_status_changed
37
+ end
38
+ end
39
+
40
+ def set_aggregated_status_bools status
41
+ raise InvalidArgument unless status.is_a? Array
42
+ raise InvalidArgument unless status.size == 8
43
+ if status != @aggregated_status_bools
44
+ @aggregated_status = []
45
+ AGGREGATED_STATUS_KEYS.each_with_index do |key,index|
46
+ on = status[index] == true
47
+ @aggregated_status_bools[index] = on
48
+ @aggregated_status << key if on
49
+ end
50
+ aggrated_status_changed
51
+ end
52
+ end
53
+
54
+ def aggrated_status_changed
55
+ @node.aggrated_status_changed self
56
+ end
57
+
58
+ def alarm code:, status:
59
+ end
60
+
61
+ def status code:, value:
62
+ end
63
+
64
+ end
65
+ end
data/lib/rsmp/error.rb ADDED
@@ -0,0 +1,44 @@
1
+ module RSMP
2
+ class Error < StandardError
3
+ end
4
+
5
+ class InvalidPacket < Error
6
+ end
7
+
8
+ class MalformedMessage < Error
9
+ end
10
+
11
+ class SchemaError < Error
12
+ end
13
+
14
+ class InvalidMessage < Error
15
+ end
16
+
17
+ class UnknownMessage < Error
18
+ end
19
+
20
+ class MissingAcknowledgment < Error
21
+ end
22
+
23
+ class MissingWatchdog < Error
24
+ end
25
+
26
+ class MissingAcknowledgment < Error
27
+ end
28
+
29
+ class MissingAttribute < InvalidMessage
30
+ end
31
+
32
+ class FatalError < Error
33
+ end
34
+
35
+ class NotReady < Error
36
+ end
37
+
38
+ class TimeoutError < Error
39
+ end
40
+
41
+ class ConnectionError < Error
42
+ end
43
+
44
+ end
@@ -0,0 +1,153 @@
1
+ module RSMP
2
+ class Logger
3
+
4
+ def initialize settings
5
+ defaults = {
6
+ 'active'=>false,
7
+ 'author'=>false,
8
+ 'color'=>true,
9
+ 'site_id'=>true,
10
+ 'component'=>false,
11
+ 'level'=>false,
12
+ 'ip'=>false,
13
+ 'index'=>false,
14
+ 'timestamp'=>true,
15
+ 'json'=>false,
16
+ 'debug'=>false,
17
+ 'statistics'=>false
18
+ }
19
+ @settings = defaults.merge settings
20
+ @muted = {}
21
+ end
22
+
23
+ def mute ip, port
24
+ @muted["#{ip}:#{port}"] = true
25
+ end
26
+
27
+ def unmute ip, port
28
+ @muted.delete "#{ip}:#{port}"
29
+ end
30
+
31
+ def unmute_all
32
+ @muted = {}
33
+ end
34
+
35
+ def output? item, force=false
36
+ return false if item[:ip] && item[:port] && @muted["#{item[:ip]}:#{item[:port]}"]
37
+ return false if @settings["active"] == false && force != true
38
+ return false if @settings["info"] == false && item[:level] == :info
39
+ return false if @settings["debug"] != true && item[:level] == :debug
40
+ return false if @settings["statistics"] != true && item[:level] == :statistics
41
+
42
+ if item[:message]
43
+ type = item[:message].type
44
+ ack = type == "MessageAck" || type == "MessageNotAck"
45
+ if @settings["watchdogs"] == false
46
+ return false if type == "Watchdog"
47
+ if ack
48
+ return false if item[:message].original && item[:message].original.type == "Watchdog"
49
+ end
50
+ end
51
+ return false if ack && @settings["acknowledgements"] == false &&
52
+ [:not_acknowledged,:warning,:error].include?(item[:level]) == false
53
+ end
54
+ true
55
+ end
56
+
57
+ def output level, str
58
+ return if str.empty? || /^\s+$/.match(str)
59
+ streams = [$stdout]
60
+ #streams << $stderr if level == :error
61
+ str = colorize level, str
62
+ streams.each do |stream|
63
+ stream.puts str
64
+ end
65
+ end
66
+
67
+ def colorize level, str
68
+ #p String.color_samples
69
+ if @settings["color"] == false || @settings["color"] == nil
70
+ str
71
+ elsif @settings["color"] == true
72
+ case level
73
+ when :error
74
+ str.colorize(:red)
75
+ when :warning
76
+ str.colorize(:light_yellow)
77
+ when :not_acknowledged
78
+ str.colorize(:cyan)
79
+ when :log
80
+ str.colorize(:light_blue)
81
+ when :statistics
82
+ str.colorize(:light_black)
83
+ else
84
+ str
85
+ end
86
+ else
87
+ if level == :nack || level == :warning || level == :error
88
+ str.colorize(@settings["color"]).bold
89
+ else
90
+ str.colorize @settings["color"]
91
+ end
92
+ end
93
+ end
94
+
95
+ def log item, force:false
96
+ if output?(item, force)
97
+ output item[:level], build_output(item)
98
+ end
99
+ end
100
+
101
+ def self.shorten_message_id m_id, length=4
102
+ if m_id
103
+ m_id[0..length-1].ljust(length)
104
+ else
105
+ ' '*length
106
+ end
107
+ end
108
+
109
+ def dump archive, force:false
110
+ archive.items.each do |item|
111
+ log item, force:force
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def build_output item
118
+ parts = []
119
+ parts << item[:index].to_s.ljust(7) if @settings["index"] == true
120
+ parts << item[:author].to_s.ljust(13) if @settings["author"] == true
121
+ parts << item[:timestamp].to_s.ljust(24) unless @settings["timestamp"] == false
122
+ parts << item[:ip].to_s.ljust(22) unless @settings["ip"] == false
123
+ parts << item[:site_id].to_s.ljust(13) unless @settings["site_id"] == false
124
+ parts << item[:component_id].to_s.ljust(18) unless @settings["component"] == false
125
+
126
+ directions = {in:"-->",out:"<--"}
127
+ parts << directions[item[:direction]].to_s.ljust(4) unless @settings["direction"] == false
128
+
129
+ parts << item[:level].to_s.capitalize.ljust(7) unless @settings["level"] == false
130
+
131
+
132
+ unless @settings["id"] == false
133
+ length = 4
134
+ if item[:message]
135
+ parts << Logger.shorten_message_id(item[:message].m_id,length)+' '
136
+ else
137
+ parts << " "*(length+1)
138
+ end
139
+ end
140
+ parts << item[:str].to_s.strip unless @settings["text"] == false
141
+ parts << item[:message].json unless @settings["json"] == false || item[:message] == nil
142
+
143
+ if item[:exception]
144
+ parts << "#{item[:exception].class.to_s}\n"
145
+ parts << item[:exception].backtrace.join("\n")
146
+ end
147
+
148
+ out = parts.join(' ').chomp(' ')
149
+ out
150
+ end
151
+
152
+ end
153
+ end
@@ -0,0 +1,313 @@
1
+ # rsmp messages
2
+ module RSMP
3
+ class Message
4
+
5
+ attr_reader :now, :attributes, :out, :timestamp
6
+ attr_accessor :json, :direction
7
+
8
+ def self.load_schemas
9
+ # path to files in submodule folder
10
+ schema_path = File.join(File.dirname(__dir__),'rsmp_schema','schema')
11
+ @@schemas = {}
12
+
13
+ core_schema_path = File.join(schema_path,'core','rsmp.json')
14
+ @@schemas['core'] = JSONSchemer.schema( Pathname.new(core_schema_path) )
15
+
16
+ tlc_schema_path = File.join(schema_path,'tlc','sxl.json')
17
+ @@schemas['traffic_light_controller'] = JSONSchemer.schema( Pathname.new(tlc_schema_path) )
18
+
19
+ @@schemas
20
+ end
21
+
22
+ def self.get_schema sxl = 'core'
23
+ schema = @@schemas[sxl]
24
+ raise SchemaError.new("Unknown schema #{sxl}") unless schema
25
+ schema
26
+ end
27
+
28
+ @@schemas = load_schemas
29
+
30
+
31
+ def self.parse_attributes packet
32
+ raise ArgumentError unless packet
33
+ JSON.parse packet
34
+ rescue JSON::ParserError
35
+ raise InvalidPacket, bin_to_chars(packet)
36
+ end
37
+
38
+ def self.build attributes, packet
39
+ validate_message_type attributes
40
+ case attributes["type"]
41
+ when "MessageAck"
42
+ message = MessageAck.new attributes
43
+ when "MessageNotAck"
44
+ message = MessageNotAck.new attributes
45
+ when "Version"
46
+ message = Version.new attributes
47
+ when "AggregatedStatus"
48
+ message = AggregatedStatus.new attributes
49
+ when "Watchdog"
50
+ message = Watchdog.new attributes
51
+ when "Alarm"
52
+ message = Alarm.new attributes
53
+ when "CommandRequest"
54
+ message = CommandRequest.new attributes
55
+ when "CommandResponse"
56
+ message = CommandResponse.new attributes
57
+ when "StatusRequest"
58
+ message = StatusRequest.new attributes
59
+ when "StatusResponse"
60
+ message = StatusResponse.new attributes
61
+ when "StatusSubscribe"
62
+ message = StatusSubscribe.new attributes
63
+ when "StatusUnsubscribe"
64
+ message = StatusUnsubscribe.new attributes
65
+ when "StatusUpdate"
66
+ message = StatusUpdate.new attributes
67
+ else
68
+ message = Unknown.new attributes
69
+ end
70
+ message.json = packet
71
+ message.direction = :in
72
+ message
73
+ end
74
+
75
+ def type
76
+ @attributes["type"]
77
+ end
78
+
79
+ def m_id
80
+ @attributes["mId"]
81
+ end
82
+
83
+ def m_id_short
84
+ @attributes["mId"][0..3]
85
+ end
86
+
87
+ def attribute key
88
+ unless @attributes.key? key # note that this is not the same as @attributes[key] when
89
+ maybe = @attributes.find { |k,v| k.downcase == key.downcase }
90
+ if maybe
91
+ raise MissingAttribute.new "attribute '#{maybe.first}' should be named '#{key}'"
92
+ else
93
+ raise MissingAttribute.new "missing attribute '#{key}'"
94
+ end
95
+ end
96
+ @attributes[key]
97
+ end
98
+
99
+ def self.bin_to_chars(s)
100
+ out = s.gsub(/[^[:print:]]/i, '.')
101
+ max = 120
102
+ if out.size <= max
103
+ out
104
+ else
105
+ mid = " ... "
106
+ length = (max-mid.size)/2 - 1
107
+ "#{out[0..length]} ... #{out[-length-1..-1]}"
108
+ end
109
+ end
110
+
111
+ def self.validate_message_type attributes
112
+ raise MalformedMessage.new("JSON must be a Hash, got #{attributes.class} ") unless attributes.is_a?(Hash)
113
+ raise MalformedMessage.new("'mType' is missing") unless attributes["mType"]
114
+ raise MalformedMessage.new("'mType' must be a String, got #{attributes["mType"].class}") unless attributes["mType"].is_a? String
115
+ raise MalformedMessage.new("'mType' must be 'rSMsg', got '#{attributes["mType"]}'") unless attributes["mType"] == "rSMsg"
116
+ raise MalformedMessage.new("'type' is missing") unless attributes["type"]
117
+ raise MalformedMessage.new("'type' must be a String, got #{attributes["type"].class}") unless attributes["type"].is_a? String
118
+ end
119
+
120
+ def initialize attributes = {}
121
+ @timestamp = RSMP.now_object
122
+ @attributes = { "mType"=> "rSMsg" }.merge attributes
123
+
124
+ ensure_message_id
125
+ end
126
+
127
+ def ensure_message_id
128
+ # if message id is empty, generate a new one
129
+ @attributes["mId"] ||= SecureRandom.uuid()
130
+ end
131
+
132
+ def validate
133
+ unless Message.get_schema.valid? attributes
134
+ errors = Message.get_schema.validate attributes
135
+ error_string = errors.map do |item|
136
+ [item['data_pointer'],item['type'],item['details']].compact.join(' ')
137
+ end.join(", ")
138
+ raise SchemaError.new error_string
139
+ end
140
+ end
141
+
142
+ def validate_type
143
+ @attributes["mType"] == "rSMsg"
144
+ end
145
+
146
+ def validate_id
147
+ (@attributes["mId"] =~ /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}/i) != nil
148
+ end
149
+
150
+ def valid?
151
+ true
152
+ end
153
+
154
+ def generate_json
155
+ @json = JSON.generate @attributes
156
+
157
+ # wrap json with a form feed to create an rsmp packet,
158
+ #as required by the rsmp specification
159
+ @out = "#{@json}"
160
+ end
161
+
162
+ end
163
+
164
+ class Malformed < Message
165
+ def initialize attributes = {}
166
+ # don't call super, just copy (potentially invalid) attributes
167
+ @attributes = {}
168
+ @invalid_attributes = attributes
169
+ end
170
+ end
171
+
172
+ class Version < Message
173
+ def initialize attributes = {}
174
+ super({
175
+ "type" => "Version",
176
+ }.merge attributes)
177
+ end
178
+
179
+ def validate
180
+ super &&
181
+ @attributes["RSMP"].is_a?(Array) && @attributes["RSMP"].size >= 1
182
+ end
183
+
184
+ def versions
185
+ attribute("RSMP").map{ |item| item["vers"] }
186
+ end
187
+ end
188
+
189
+ class Unknown < Message
190
+ end
191
+
192
+ class AggregatedStatus < Message
193
+ def initialize attributes = {}
194
+ super({
195
+ "type" => "AggregatedStatus",
196
+ }.merge attributes)
197
+ end
198
+ end
199
+
200
+ class Alarm < Message
201
+ def initialize attributes = {}
202
+ super({
203
+ "type" => "Alarm",
204
+ }.merge attributes)
205
+ end
206
+ end
207
+
208
+ class Watchdog < Message
209
+ def initialize attributes = {}
210
+ super({
211
+ "type" => "Watchdog",
212
+ }.merge attributes)
213
+ end
214
+ end
215
+
216
+ class MessageAcking < Message
217
+ attr_reader :original
218
+
219
+ def self.build_from message
220
+ return new({
221
+ "oMId" => message.attributes["mId"]
222
+ })
223
+ end
224
+
225
+ def original= message
226
+ raise InvalidArgument unless message
227
+ @original = message
228
+ end
229
+
230
+ def validate_id
231
+ true
232
+ end
233
+ end
234
+
235
+ class MessageAck < MessageAcking
236
+ def initialize attributes = {}
237
+ super({
238
+ "type" => "MessageAck",
239
+ }.merge attributes)
240
+ end
241
+
242
+ def ensure_message_id
243
+ # Ack and NotAck does not have a mId
244
+ end
245
+ end
246
+
247
+ class MessageNotAck < MessageAcking
248
+ def initialize attributes = {}
249
+ super({
250
+ "type" => "MessageNotAck",
251
+ "rea" => "Unknown reason"
252
+ }.merge attributes)
253
+ @attributes.delete "mId"
254
+ end
255
+ end
256
+
257
+ class CommandRequest < Message
258
+ def initialize attributes = {}
259
+ super({
260
+ "type" => "CommandRequest",
261
+ }.merge attributes)
262
+ end
263
+ end
264
+
265
+ class CommandResponse < Message
266
+ def initialize attributes = {}
267
+ super({
268
+ "type" => "CommandResponse",
269
+ }.merge attributes)
270
+ end
271
+ end
272
+
273
+ class StatusRequest < Message
274
+ def initialize attributes = {}
275
+ super({
276
+ "type" => "StatusRequest",
277
+ }.merge attributes)
278
+ end
279
+ end
280
+
281
+ class StatusResponse < Message
282
+ def initialize attributes = {}
283
+ super({
284
+ "type" => "StatusResponse",
285
+ }.merge attributes)
286
+ end
287
+ end
288
+
289
+ class StatusSubscribe < Message
290
+ def initialize attributes = {}
291
+ super({
292
+ "type" => "StatusSubscribe",
293
+ }.merge attributes)
294
+ end
295
+ end
296
+
297
+ class StatusUnsubscribe < Message
298
+ def initialize attributes = {}
299
+ super({
300
+ "type" => "StatusUnsubscribe",
301
+ }.merge attributes)
302
+ end
303
+ end
304
+
305
+ class StatusUpdate < Message
306
+ def initialize attributes = {}
307
+ super({
308
+ "type" => "StatusUpdate",
309
+ }.merge attributes)
310
+ end
311
+ end
312
+
313
+ end
data/lib/rsmp/node.rb ADDED
@@ -0,0 +1,53 @@
1
+ # RSMP site
2
+ #
3
+ # Handles a single connection to a supervisor.
4
+ # We connect to the supervisor.
5
+
6
+ module RSMP
7
+ class Node < Base
8
+ attr_reader :archive, :logger, :task
9
+
10
+ def initialize options
11
+ super options
12
+ end
13
+
14
+ def start
15
+ starting
16
+ Async do |task|
17
+ task.annotate self.class
18
+ @task = task
19
+ start_action
20
+ end
21
+ rescue Errno::EADDRINUSE => e
22
+ log "Cannot start: #{e.to_s}", level: :error
23
+ rescue SystemExit, SignalException, Interrupt
24
+ @logger.unmute_all
25
+ exiting
26
+ end
27
+
28
+ def stop
29
+ @task.stop if @task
30
+ end
31
+
32
+ def restart
33
+ stop
34
+ start
35
+ end
36
+
37
+ def exiting
38
+ log "Exiting", level: :info
39
+ end
40
+
41
+ def check_required_settings settings, required
42
+ raise ArgumentError.new "Settings is empty" unless settings
43
+ required.each do |setting|
44
+ raise ArgumentError.new "Missing setting: #{setting}" unless settings.include? setting.to_s
45
+ end
46
+ end
47
+
48
+ def author
49
+ site_id
50
+ end
51
+
52
+ end
53
+ end