rsmp 0.1.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.
@@ -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