openc3 5.20.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +12 -120
  3. data/data/config/command_modifiers.yaml +13 -1
  4. data/data/config/interface_modifiers.yaml +21 -4
  5. data/data/config/item_modifiers.yaml +1 -1
  6. data/data/config/microservice.yaml +15 -2
  7. data/data/config/param_item_modifiers.yaml +1 -1
  8. data/data/config/parameter_modifiers.yaml +1 -1
  9. data/data/config/table_manager.yaml +2 -2
  10. data/data/config/target.yaml +11 -0
  11. data/data/config/telemetry_modifiers.yaml +17 -1
  12. data/data/config/tool.yaml +12 -0
  13. data/data/config/widgets.yaml +13 -17
  14. data/lib/openc3/accessors/form_accessor.rb +4 -3
  15. data/lib/openc3/accessors/html_accessor.rb +3 -3
  16. data/lib/openc3/accessors/http_accessor.rb +13 -13
  17. data/lib/openc3/accessors/xml_accessor.rb +16 -4
  18. data/lib/openc3/api/target_api.rb +0 -30
  19. data/lib/openc3/config/config_parser.rb +6 -3
  20. data/lib/openc3/core_ext/array.rb +0 -16
  21. data/lib/openc3/core_ext.rb +0 -1
  22. data/lib/openc3/interfaces/file_interface.rb +198 -0
  23. data/lib/openc3/interfaces/http_client_interface.rb +71 -39
  24. data/lib/openc3/interfaces/http_server_interface.rb +0 -7
  25. data/lib/openc3/interfaces/interface.rb +2 -0
  26. data/lib/openc3/interfaces/mqtt_interface.rb +32 -15
  27. data/lib/openc3/interfaces/mqtt_stream_interface.rb +19 -4
  28. data/lib/openc3/interfaces/protocols/crc_protocol.rb +7 -0
  29. data/lib/openc3/interfaces/serial_interface.rb +1 -0
  30. data/lib/openc3/interfaces.rb +2 -4
  31. data/lib/openc3/microservices/multi_microservice.rb +3 -3
  32. data/lib/openc3/migrations/20241208080000_no_critical_cmd.rb +31 -0
  33. data/lib/openc3/migrations/20241208080001_no_trigger_group.rb +46 -0
  34. data/lib/openc3/models/interface_model.rb +9 -3
  35. data/lib/openc3/models/microservice_model.rb +8 -1
  36. data/lib/openc3/models/plugin_model.rb +6 -1
  37. data/lib/openc3/models/python_package_model.rb +6 -1
  38. data/lib/openc3/models/reaction_model.rb +14 -10
  39. data/lib/openc3/models/scope_model.rb +60 -42
  40. data/lib/openc3/models/target_model.rb +17 -1
  41. data/lib/openc3/models/timeline_model.rb +17 -5
  42. data/lib/openc3/models/tool_model.rb +15 -3
  43. data/lib/openc3/models/trigger_group_model.rb +6 -3
  44. data/lib/openc3/operators/microservice_operator.rb +8 -0
  45. data/lib/openc3/packets/commands.rb +17 -6
  46. data/lib/openc3/packets/limits.rb +0 -12
  47. data/lib/openc3/packets/packet.rb +1 -1
  48. data/lib/openc3/packets/packet_item.rb +30 -36
  49. data/lib/openc3/packets/structure_item.rb +2 -2
  50. data/lib/openc3/script/script.rb +0 -10
  51. data/lib/openc3/script/web_socket_api.rb +2 -2
  52. data/lib/openc3/streams/mqtt_stream.rb +41 -33
  53. data/lib/openc3/streams/serial_stream.rb +27 -27
  54. data/lib/openc3/streams/stream.rb +17 -17
  55. data/lib/openc3/streams/tcpip_client_stream.rb +1 -1
  56. data/lib/openc3/streams/tcpip_socket_stream.rb +19 -19
  57. data/lib/openc3/system/system.rb +1 -1
  58. data/lib/openc3/system.rb +2 -3
  59. data/lib/openc3/tools/table_manager/table.rb +2 -2
  60. data/lib/openc3/tools/table_manager/table_parser.rb +1 -1
  61. data/lib/openc3/top_level.rb +0 -5
  62. data/lib/openc3/topics/command_decom_topic.rb +0 -7
  63. data/lib/openc3/utilities/bucket_utilities.rb +1 -1
  64. data/lib/openc3/utilities/cli_generator.rb +0 -1
  65. data/lib/openc3/version.rb +6 -6
  66. data/templates/plugin/README.md +1 -1
  67. data/templates/target/targets/TARGET/lib/target.rb +1 -1
  68. data/templates/tool_angular/package.json +8 -8
  69. data/templates/tool_angular/src/app/app.component.html +4 -13
  70. data/templates/tool_angular/src/app/app.component.scss +5 -13
  71. data/templates/tool_angular/src/app/app.component.ts +5 -4
  72. data/templates/tool_angular/src/app/custom-overlay-container.ts +2 -2
  73. data/templates/tool_angular/src/app/openc3-api.d.ts +1 -1
  74. data/templates/tool_angular/src/main.single-spa.ts +1 -1
  75. data/templates/tool_react/package.json +1 -0
  76. data/templates/tool_react/src/root.component.js +1 -1
  77. data/templates/tool_svelte/package.json +11 -9
  78. data/templates/tool_svelte/rollup.config.js +2 -0
  79. data/templates/tool_svelte/src/App.svelte +2 -2
  80. data/templates/tool_vue/eslint.config.mjs +68 -0
  81. data/templates/tool_vue/jsconfig.json +1 -1
  82. data/templates/tool_vue/package.json +26 -43
  83. data/templates/tool_vue/src/App.vue +3 -5
  84. data/templates/tool_vue/src/main.js +12 -23
  85. data/templates/tool_vue/src/router.js +19 -18
  86. data/templates/tool_vue/src/tools/tool_name/tool_name.vue +2 -2
  87. data/templates/tool_vue/vite.config.js +52 -0
  88. data/templates/widget/package.json +19 -26
  89. data/templates/widget/src/Widget.vue +13 -15
  90. data/templates/widget/vite.config.js +26 -0
  91. metadata +10 -41
  92. data/lib/openc3/core_ext/hash.rb +0 -40
  93. data/lib/openc3/core_ext/httpclient.rb +0 -11
  94. data/lib/openc3/interfaces/linc_interface.rb +0 -480
  95. data/lib/openc3/interfaces/protocols/override_protocol.rb +0 -4
  96. data/lib/openc3/microservices/critical_cmd_microservice.rb +0 -74
  97. data/lib/openc3/microservices/reaction_microservice.rb +0 -607
  98. data/lib/openc3/microservices/timeline_microservice.rb +0 -398
  99. data/lib/openc3/microservices/trigger_group_microservice.rb +0 -698
  100. data/lib/openc3/migrations/20230615000000_autonomic.rb +0 -86
  101. data/lib/openc3/migrations/20240915000000_activity_uuid.rb +0 -28
  102. data/lib/openc3/migrations/20241016000000_scope_critical_cmd.rb +0 -24
  103. data/lib/openc3/system/system_config.rb +0 -413
  104. data/templates/tool_svelte/src/services/api.js +0 -92
  105. data/templates/tool_svelte/src/services/axios.js +0 -85
  106. data/templates/tool_svelte/src/services/cable.js +0 -65
  107. data/templates/tool_svelte/src/services/config-parser.js +0 -198
  108. data/templates/tool_svelte/src/services/openc3-api.js +0 -606
  109. data/templates/tool_vue/.eslintrc.js +0 -43
  110. data/templates/tool_vue/babel.config.json +0 -11
  111. data/templates/tool_vue/vue.config.js +0 -38
  112. data/templates/widget/.eslintrc.js +0 -43
  113. data/templates/widget/babel.config.json +0 -11
  114. data/templates/widget/vue.config.js +0 -28
  115. /data/templates/tool_vue/{.prettierrc.js → .prettierrc.cjs} +0 -0
  116. /data/templates/widget/{.prettierrc.js → .prettierrc.cjs} +0 -0
@@ -1,698 +0,0 @@
1
- # encoding: ascii-8bit
2
-
3
- # Copyright 2022 Ball Aerospace & Technologies Corp.
4
- # All Rights Reserved.
5
- #
6
- # This program is free software; you can modify and/or redistribute it
7
- # under the terms of the GNU Affero General Public License
8
- # as published by the Free Software Foundation; version 3 with
9
- # attribution addendums as found in the LICENSE.txt
10
- #
11
- # This program is distributed in the hope that it will be useful,
12
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- # GNU Affero General Public License for more details.
15
-
16
- # Modified by OpenC3, Inc.
17
- # All changes Copyright 2024, OpenC3, Inc.
18
- # All Rights Reserved
19
- #
20
- # This file may also be used under the terms of a commercial license
21
- # if purchased from OpenC3, Inc.
22
-
23
- require 'openc3/microservices/microservice'
24
- require 'openc3/models/trigger_model'
25
- require 'openc3/topics/autonomic_topic'
26
- require 'openc3/utilities/authentication'
27
- require 'openc3/packets/json_packet'
28
-
29
- require 'openc3/script'
30
-
31
- module OpenC3
32
- class TriggerLoopError < TriggerError; end
33
-
34
- # Stored in the TriggerGroupShare this should be a thread safe
35
- # hash that triggers will be added, updated, and removed from
36
- class PacketBase
37
- def initialize(scope:)
38
- @scope = scope
39
- @mutex = Mutex.new
40
- @packets = Hash.new
41
- end
42
-
43
- def packet(target:, packet:)
44
- topic = "#{@scope}__DECOM__{#{target}}__#{packet}"
45
- @mutex.synchronize do
46
- return nil unless @packets[topic]
47
- # Deep copy the packet so it doesn't change under us
48
- return Marshal.load( Marshal.dump(@packets[topic][-1]) )
49
- end
50
- end
51
-
52
- def previous_packet(target:, packet:)
53
- topic = "#{@scope}__DECOM__{#{target}}__#{packet}"
54
- @mutex.synchronize do
55
- return nil unless @packets[topic] and @packets[topic].length == 2
56
- # Deep copy the packet so it doesn't change under us
57
- return Marshal.load( Marshal.dump(@packets[topic][0]) )
58
- end
59
- end
60
-
61
- def add(topic:, packet:)
62
- @mutex.synchronize do
63
- @packets[topic] ||= []
64
- if @packets[topic].length == 2
65
- @packets[topic].shift
66
- end
67
- @packets[topic].push(packet)
68
- end
69
- end
70
-
71
- def remove(topic:)
72
- @mutex.synchronize do
73
- @packets.delete(topic)
74
- end
75
- end
76
- end
77
-
78
- # Stored in the TriggerGroupShare this should be a thread safe
79
- # hash that triggers will be added, updated, and removed from.
80
- class TriggerBase
81
- attr_reader :autonomic_topic, :triggers
82
-
83
- def initialize(scope:)
84
- @scope = scope
85
- @autonomic_topic = "#{@scope}__openc3_autonomic".freeze
86
- @triggers_mutex = Mutex.new
87
- @triggers = Hash.new
88
- @lookup_mutex = Mutex.new
89
- @lookup = Hash.new
90
- end
91
-
92
- # Get triggers to evaluate based on the topic. If the
93
- # topic is equal to the autonomic topic it will
94
- # return only triggers with roots
95
- def get_triggers(topic:)
96
- if @autonomic_topic == topic
97
- return triggers_with_roots()
98
- else
99
- return triggers_from(topic: topic)
100
- end
101
- end
102
-
103
- # update trigger state after evaluated
104
- # -1 (the value is considered an error used to disable the trigger)
105
- # 0 (the value is considered as a false value)
106
- # 1 (the value is considered as a true value)
107
- def update_state(name:, value:)
108
- @triggers_mutex.synchronize do
109
- data = @triggers[name]
110
- return unless data
111
- trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
112
- if value == -1 && trigger.enabled
113
- trigger.disable()
114
- elsif value == 1 && trigger.state == false
115
- trigger.state = true
116
- elsif value == 0 && trigger.state == true
117
- trigger.state = false
118
- end
119
- @triggers[name] = trigger.as_json(:allow_nan => true)
120
- end
121
- end
122
-
123
- # returns a Hash of ALL enabled Trigger objects
124
- def enabled_triggers
125
- val = nil
126
- @triggers_mutex.synchronize do
127
- val = Marshal.load( Marshal.dump(@triggers) )
128
- end
129
- ret = Hash.new
130
- val.each do | name, data |
131
- trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
132
- ret[name] = trigger if trigger.enabled
133
- end
134
- return ret
135
- end
136
-
137
- # returns an Array of enabled Trigger objects that have roots to other triggers
138
- def triggers_with_roots
139
- val = nil
140
- @triggers_mutex.synchronize do
141
- val = Marshal.load( Marshal.dump(@triggers) )
142
- end
143
- ret = []
144
- val.each do | _name, data |
145
- trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
146
- ret << trigger if trigger.enabled && ! trigger.roots.empty?
147
- end
148
- return ret
149
- end
150
-
151
- # returns an Array of enabled Trigger objects that use a topic
152
- def triggers_from(topic:)
153
- val = nil
154
- @lookup_mutex.synchronize do
155
- val = Marshal.load( Marshal.dump(@lookup[topic]) )
156
- end
157
- return [] if val.nil?
158
- ret = []
159
- @triggers_mutex.synchronize do
160
- val.each do | trigger_name |
161
- data = Marshal.load( Marshal.dump(@triggers[trigger_name]) )
162
- trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
163
- ret << trigger if trigger.enabled
164
- end
165
- end
166
- return ret
167
- end
168
-
169
- # get all topics group is working with
170
- def topics
171
- @lookup_mutex.synchronize do
172
- return Marshal.load( Marshal.dump(@lookup.keys()) )
173
- end
174
- end
175
-
176
- # Rebuild the database lookup of all triggers in the group
177
- def rebuild(triggers:)
178
- @triggers_mutex.synchronize do
179
- @triggers = Marshal.load( Marshal.dump(triggers) )
180
- end
181
- @lookup_mutex.synchronize do
182
- @lookup = { @autonomic_topic => [] }
183
- triggers.each do | _name, data |
184
- trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
185
- trigger.generate_topics.each do | topic |
186
- @lookup[topic] ||= []
187
- @lookup[topic] << trigger.name
188
- end
189
- end
190
- end
191
- end
192
-
193
- # Add a trigger from TriggerBase, must only be called once per trigger
194
- def add(trigger:)
195
- @triggers_mutex.synchronize do
196
- @triggers[trigger['name']] = Marshal.load( Marshal.dump(trigger) )
197
- end
198
- trigger = TriggerModel.from_json(trigger, name: trigger['name'], scope: trigger['scope'])
199
- @lookup_mutex.synchronize do
200
- trigger.generate_topics.each do | topic |
201
- @lookup[topic] ||= []
202
- @lookup[topic] << trigger.name
203
- end
204
- end
205
- end
206
-
207
- # update a trigger from TriggerBase
208
- def update(trigger:)
209
- @triggers_mutex.synchronize do
210
- model = TriggerModel.from_json(trigger, name: trigger['name'], scope: trigger['scope'])
211
- model.update()
212
- @triggers[trigger['name']] = model.as_json(:allow_nan => true)
213
- end
214
- end
215
-
216
- # remove a trigger from TriggerBase
217
- def remove(trigger:)
218
- topics = []
219
- @triggers_mutex.synchronize do
220
- @triggers.delete(trigger['name'])
221
- model = TriggerModel.from_json(trigger, name: trigger['name'], scope: trigger['scope'])
222
- topics = model.generate_topics()
223
- TriggerModel.delete(name: trigger['name'], group: trigger['group'], scope: trigger['scope'])
224
- end
225
- @lookup_mutex.synchronize do
226
- topics.each do | topic |
227
- unless @lookup[topic].nil?
228
- @lookup[topic].delete(trigger['name'])
229
- @lookup.delete(topic) if @lookup[topic].empty?
230
- end
231
- end
232
- end
233
- end
234
- end
235
-
236
- # Shared between the monitor thread and the manager thread to
237
- # share the triggers. This should remain a thread
238
- # safe implementation.
239
- class TriggerGroupShare
240
- attr_reader :trigger_base, :packet_base
241
-
242
- def initialize(scope:)
243
- @scope = scope
244
- @trigger_base = TriggerBase.new(scope: scope)
245
- @packet_base = PacketBase.new(scope: scope)
246
- end
247
- end
248
-
249
- # The TriggerGroupWorker is a very simple thread pool worker. Once
250
- # the trigger manager has pushed a packet to the queue one of
251
- # these workers will evaluate the triggers for that packet.
252
- class TriggerGroupWorker
253
- TYPE = 'type'.freeze
254
- ITEM_TARGET = 'target'.freeze
255
- ITEM_PACKET = 'packet'.freeze
256
- ITEM_TYPE = 'item'.freeze
257
- ITEM_VALUE_TYPE = 'valueType'.freeze
258
- FLOAT_TYPE = 'float'.freeze
259
- STRING_TYPE = 'string'.freeze
260
- REGEX_TYPE = 'regex'.freeze
261
- LIMIT_TYPE = 'limit'.freeze
262
- TRIGGER_TYPE = 'trigger'.freeze
263
-
264
- attr_reader :name, :scope, :target, :packet, :group
265
-
266
- def initialize(name:, logger:, scope:, group:, queue:, share:, ident:)
267
- @name = name
268
- @logger = logger
269
- @scope = scope
270
- @group = group
271
- @queue = queue
272
- @share = share
273
- @ident = ident
274
- end
275
-
276
- def notify(name:, severity:, message:)
277
- data = {}
278
- # All AutonomicTopic notifications must have 'name' and 'updated_at' in the data
279
- data['name'] = name
280
- data['updated_at'] = Time.now.to_nsec_from_epoch
281
- data['severity'] = severity
282
- data['message'] = message
283
- notification = {
284
- 'kind' => 'error',
285
- 'type' => 'trigger',
286
- 'data' => JSON.generate(data),
287
- }
288
- AutonomicTopic.write_notification(notification, scope: @scope)
289
- @logger.public_send(severity.intern, message)
290
- end
291
-
292
- def run
293
- @logger.info "TriggerGroupWorker-#{@ident} running"
294
- loop do
295
- topic = @queue.pop
296
- break if topic.nil?
297
- begin
298
- evaluate_data_packet(topic: topic)
299
- rescue StandardError => e
300
- @logger.error "TriggerGroupWorker-#{@ident} failed to evaluate data packet from topic: #{topic}\n#{e.formatted}"
301
- end
302
- end
303
- @logger.info "TriggerGroupWorker-#{@ident} exiting"
304
- end
305
-
306
- # Each packet will be evaluated to all triggers and use the result to send
307
- # the results back to the topic to be used by the reaction microservice.
308
- def evaluate_data_packet(topic:)
309
- visited = Hash.new
310
- @logger.debug "TriggerGroupWorker-#{@ident} topic: #{topic}"
311
- @share.trigger_base.get_triggers(topic: topic).each do |trigger|
312
- @logger.debug "TriggerGroupWorker-#{@ident} eval head: #{trigger}"
313
- value = evaluate_trigger(
314
- head: trigger,
315
- trigger: trigger,
316
- visited: visited,
317
- triggers: @share.trigger_base.enabled_triggers
318
- )
319
- @logger.debug "TriggerGroupWorker-#{@ident} trigger: #{trigger} value: #{value}"
320
- # value MUST be -1, 0, or 1
321
- @share.trigger_base.update_state(name: trigger.name, value: value)
322
- end
323
- end
324
-
325
- # extract the value outlined in the operand to get the packet item limit
326
- # IF operand limit does not include _LOW or _HIGH this will match the
327
- # COLOR and return COLOR_LOW || COLOR_HIGH
328
- # operand item: GREEN_LOW == other operand limit: GREEN
329
- def get_packet_limit(operand:, other:)
330
- packet = @share.packet_base.packet(
331
- target: operand[ITEM_TARGET],
332
- packet: operand[ITEM_PACKET]
333
- )
334
- return nil if packet.nil?
335
- _, limit = packet.read_with_limits_state(operand[ITEM_TYPE], operand[ITEM_VALUE_TYPE].intern)
336
- # Convert limit symbol to string since we'll be comparing with strings
337
- return limit.to_s
338
- end
339
-
340
- # extract the value outlined in the operand to get the packet item value
341
- # IF raw in operand it will pull the raw value over the converted
342
- def get_packet_value(operand:, previous:)
343
- if previous
344
- packet = @share.packet_base.previous_packet(
345
- target: operand[ITEM_TARGET],
346
- packet: operand[ITEM_PACKET]
347
- )
348
- # Previous might not be populated ... that's ok just return nil
349
- return nil unless packet
350
- else
351
- packet = @share.packet_base.packet(
352
- target: operand[ITEM_TARGET],
353
- packet: operand[ITEM_PACKET]
354
- )
355
- end
356
- # This shouldn't happen because the frontend provides valid items but good to check
357
- # The raise is ultimately rescued inside evaluate_trigger when operand_value is called
358
- if packet.nil?
359
- raise "Packet #{operand[ITEM_TARGET]} #{operand[ITEM_PACKET]} not found"
360
- end
361
- value = packet.read(operand[ITEM_TYPE], operand[ITEM_VALUE_TYPE].intern)
362
- if value.nil?
363
- raise "Item #{operand[ITEM_TARGET]} #{operand[ITEM_PACKET]} #{operand[ITEM_TYPE]} not found"
364
- end
365
- value
366
- end
367
-
368
- # extract the value of the operand from the packet
369
- def operand_value(operand:, other:, visited:, previous: false)
370
- if operand[TYPE] == ITEM_TYPE && other && other[TYPE] == LIMIT_TYPE
371
- return get_packet_limit(operand: operand, other: other)
372
- elsif operand[TYPE] == ITEM_TYPE
373
- return get_packet_value(operand: operand, previous: previous)
374
- elsif operand[TYPE] == TRIGGER_TYPE
375
- return visited["#{operand[TRIGGER_TYPE]}__R"] == 1
376
- elsif operand[TYPE] == FLOAT_TYPE
377
- return operand[operand[TYPE]].to_f
378
- elsif operand[TYPE] == STRING_TYPE
379
- return operand[operand[TYPE]].to_s
380
- elsif operand[TYPE] == REGEX_TYPE
381
- # This can potentially throw an exception on badly formatted Regexp
382
- return Regexp.new(operand[operand[TYPE]])
383
- elsif operand[TYPE] == LIMIT_TYPE
384
- return operand[operand[TYPE]]
385
- else
386
- # This is a logic error ... should never get here
387
- raise "Unknown operand type: #{operand}"
388
- end
389
- end
390
-
391
- # the base evaluate method used by evaluate_trigger
392
- # -1 (the value is considered an error used to disable the trigger)
393
- # 0 (the value is considered as a false value)
394
- # 1 (the value is considered as a true value)
395
- #
396
- def evaluate(name:, left:, operator:, right:)
397
- @logger.debug "TriggerGroupWorker-#{@ident} evaluate: (#{left}(#{left.class}) #{operator} #{right}(#{right.class}))"
398
- begin
399
- case operator
400
- when '>'
401
- return left > right ? 1 : 0
402
- when '<'
403
- return left < right ? 1 : 0
404
- when '>='
405
- return left >= right ? 1 : 0
406
- when '<='
407
- return left <= right ? 1 : 0
408
- when '!=', 'CHANGES'
409
- return left != right ? 1 : 0
410
- when '==', 'DOES NOT CHANGE'
411
- return left == right ? 1 : 0
412
- when '!~'
413
- return left !~ right ? 1 : 0
414
- when '=~'
415
- return left =~ right ? 1 : 0
416
- when 'AND'
417
- return left && right ? 1 : 0
418
- when 'OR'
419
- return left || right ? 1 : 0
420
- end
421
- rescue ArgumentError
422
- message = "invalid evaluate: (#{left} #{operator} #{right})"
423
- notify(name: name, severity: 'error', message: message)
424
- return -1
425
- end
426
- end
427
-
428
- # This could be confusing... So this is a recursive method for the
429
- # TriggerGroupWorkers to call. It will use the trigger name and append a
430
- # __P for path or __R for result. The Path is a Hash that contains
431
- # a key for each node traveled to get results. When the result has
432
- # been found it will be stored in the result key __R in the visited Hash
433
- # and eval_trigger will return a number.
434
- # -1 (the value is considered an error used to disable the trigger)
435
- # 0 (the value is considered as a false value)
436
- # 1 (the value is considered as a true value)
437
- #
438
- # IF an operand is evaluated as nil it will log an error and return -1
439
- # IF a loop is detected it will log an error and return -1
440
- def evaluate_trigger(head:, trigger:, visited:, triggers:)
441
- if visited["#{trigger.name}__R"]
442
- return visited["#{trigger.name}__R"]
443
- end
444
- if visited["#{trigger.name}__P"].nil?
445
- visited["#{trigger.name}__P"] = Hash.new
446
- end
447
- if visited["#{head.name}__P"][trigger.name]
448
- # Not sure if this is possible as on create it validates that the dependents are already created
449
- message = "loop detected from #{head.name} -> #{trigger.name} path: #{visited["#{head.name}__P"]}"
450
- notify(name: trigger.name, severity: 'error', message: message)
451
- return visited["#{trigger.name}__R"] = -1
452
- end
453
- trigger.roots.each do | root_trigger_name |
454
- next if visited["#{root_trigger_name}__R"]
455
- root_trigger = triggers[root_trigger_name]
456
- if head.name == root_trigger.name
457
- message = "loop detected from #{head.name} -> #{root_trigger_name} path: #{visited["#{head.name}__P"]}"
458
- notify(name: trigger.name, severity: 'error', message: message)
459
- return visited["#{trigger.name}__R"] = -1
460
- end
461
- result = evaluate_trigger(
462
- head: head,
463
- trigger: root_trigger,
464
- visited: visited,
465
- triggers: triggers
466
- )
467
- @logger.debug "TriggerGroupWorker-#{@ident} #{root_trigger.name} result: #{result}"
468
- visited["#{root_trigger.name}__R"] = visited["#{head.name}__P"][root_trigger.name] = result
469
- end
470
- begin
471
- left = operand_value(operand: trigger.left, other: trigger.right, visited: visited)
472
- if trigger.operator.include?('CHANGE')
473
- right = operand_value(operand: trigger.left, other: trigger.right, visited: visited, previous: true)
474
- else
475
- right = operand_value(operand: trigger.right, other: trigger.left, visited: visited)
476
- end
477
- rescue => e
478
- # This will primarily happen when the user inputs a bad Regexp
479
- notify(name: trigger.name, severity: 'error', message: e.message)
480
- return visited["#{trigger.name}__R"] = -1
481
- end
482
- # Convert the standard '==' and '!=' into Ruby Regexp operators
483
- operator = trigger.operator
484
- if right and right.is_a? Regexp
485
- operator = '=~' if operator == '=='
486
- operator = '!~' if operator == '!='
487
- end
488
- if left.nil? || right.nil?
489
- return visited["#{trigger.name}__R"] = 0
490
- end
491
- result = evaluate(name: trigger.name,left: left, operator: operator, right: right)
492
- return visited["#{trigger.name}__R"] = result
493
- end
494
- end
495
-
496
- # The trigger manager starts a thread pool and subscribes
497
- # to the telemetry decom topic. It adds the "packet" to the thread pool queue
498
- # and the thread will evaluate the "trigger".
499
- class TriggerGroupManager
500
- attr_reader :name, :scope, :share, :group, :topics, :thread_pool
501
-
502
- def initialize(name:, logger:, scope:, group:, share:)
503
- @name = name
504
- @logger = logger
505
- @scope = scope
506
- @group = group
507
- @share = share
508
- @worker_count = 3
509
- @queue = Queue.new
510
- @read_topic = true
511
- @topics = []
512
- @thread_pool = nil
513
- @cancel_thread = false
514
- end
515
-
516
- def generate_thread_pool()
517
- thread_pool = []
518
- @worker_count.times do | i |
519
- worker = TriggerGroupWorker.new(
520
- name: @name,
521
- logger: @logger,
522
- scope: @scope,
523
- group: @group,
524
- queue: @queue,
525
- share: @share,
526
- ident: i,
527
- )
528
- thread_pool << Thread.new { worker.run }
529
- end
530
- return thread_pool
531
- end
532
-
533
- def run
534
- @logger.info "TriggerGroupManager running"
535
- @thread_pool = generate_thread_pool()
536
- loop do
537
- begin
538
- update_topics()
539
- rescue StandardError => e
540
- @logger.error "TriggerGroupManager failed to update topics.\n#{e.formatted}"
541
- end
542
- break if @cancel_thread
543
- block_for_updates()
544
- break if @cancel_thread
545
- end
546
- @logger.info "TriggerGroupManager exiting"
547
- end
548
-
549
- def update_topics
550
- past_topics = @topics
551
- @topics = @share.trigger_base.topics()
552
- @logger.debug "TriggerGroupManager past_topics: #{past_topics} topics: #{@topics}"
553
- (past_topics - @topics).each do | removed_topic |
554
- @share.packet_base.remove(topic: removed_topic)
555
- end
556
- end
557
-
558
- def block_for_updates
559
- @read_topic = true
560
- while @read_topic
561
- begin
562
- Topic.read_topics(@topics) do |topic, _msg_id, msg_hash, _redis|
563
- @logger.debug "TriggerGroupManager block_for_updates: #{topic} #{msg_hash}"
564
- if topic != @share.trigger_base.autonomic_topic
565
- packet = JsonPacket.new(:TLM, msg_hash['target_name'], msg_hash['packet_name'], msg_hash['time'].to_i, false, msg_hash["json_data"])
566
- @share.packet_base.add(topic: topic, packet: packet)
567
- end
568
- @queue << "#{topic}"
569
- end
570
- rescue StandardError => e
571
- @logger.error "TriggerGroupManager failed to read topics #{@topics}\n#{e.formatted}"
572
- end
573
- end
574
- end
575
-
576
- def refresh
577
- @read_topic = false
578
- end
579
-
580
- def shutdown
581
- @read_topic = false
582
- @cancel_thread = true
583
- @worker_count.times do | _i |
584
- @queue << nil
585
- end
586
- end
587
- end
588
-
589
- # The trigger microservice starts a manager then gets the activities
590
- # from the sorted set in redis and updates the schedule for the
591
- # manager. Timeline will then wait for an update on the timeline
592
- # stream this will trigger an update again to the schedule.
593
- class TriggerGroupMicroservice < Microservice
594
- attr_reader :name, :scope, :share, :group, :manager, :manager_thread
595
- # This lookup is mapping all the different trigger notifications
596
- # which are primarily sent by notify in TriggerModel
597
- TOPIC_LOOKUP = {
598
- 'error' => :no_op, # Sent by TriggerGroupWorker
599
- 'created' => :created_trigger_event,
600
- 'updated' => :rebuild_trigger_event,
601
- 'deleted' => :deleted_trigger_event,
602
- 'enabled' => :updated_trigger_event,
603
- 'disabled' => :updated_trigger_event,
604
- 'true' => :no_op, # Sent by TriggerGroupWorker
605
- 'false' => :no_op, # Sent by TriggerGroupWorker
606
- }
607
-
608
- def initialize(*args)
609
- super(*args)
610
- # The name is passed in via the trigger_group_model as "#{scope}__TRIGGER_GROUP__#{name}"
611
- @group = @name.split('__')[2]
612
- @share = TriggerGroupShare.new(scope: @scope)
613
- @manager = TriggerGroupManager.new(name: @name, logger: @logger, scope: @scope, group: @group, share: @share)
614
- @manager_thread = nil
615
- @read_topic = true
616
- end
617
-
618
- def run
619
- @logger.info "TriggerGroupMicroservice running"
620
- @manager_thread = Thread.new { @manager.run }
621
- loop do
622
- triggers = TriggerModel.all(scope: @scope, group: @group)
623
- @share.trigger_base.rebuild(triggers: triggers)
624
- @manager.refresh() # Every time we do a full base update we refresh the manager
625
- break if @cancel_thread
626
- block_for_updates()
627
- break if @cancel_thread
628
- end
629
- @logger.info "TriggerGroupMicroservice exiting"
630
- end
631
-
632
- def block_for_updates
633
- @read_topic = true
634
- while @read_topic && !@cancel_thread
635
- begin
636
- AutonomicTopic.read_topics(@topics) do |_topic, _msg_id, msg_hash, _redis|
637
- break if @cancel_thread
638
- @logger.debug "TriggerGroupMicroservice block_for_updates: #{msg_hash}"
639
- # Process trigger notifications created by TriggerModel notify
640
- if msg_hash['type'] == 'trigger'
641
- data = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
642
- public_send(TOPIC_LOOKUP[msg_hash['kind']], data)
643
- end
644
- end
645
- rescue StandardError => e
646
- @logger.error "TriggerGroupMicroservice failed to read topics #{@topics}\n#{e.formatted}"
647
- end
648
- end
649
- end
650
-
651
- def no_op(data)
652
- @logger.debug "TriggerGroupMicroservice web socket event: #{data}"
653
- end
654
-
655
- # Add the trigger to the share.
656
- def created_trigger_event(data)
657
- @logger.debug "TriggerGroupMicroservice created_trigger_event #{data}"
658
- if data['group'] == @group
659
- @share.trigger_base.add(trigger: data)
660
- @manager.refresh()
661
- end
662
- end
663
-
664
- def updated_trigger_event(data)
665
- @logger.debug "TriggerGroupMicroservice updated_trigger_event #{data}"
666
- if data['group'] == @group
667
- @share.trigger_base.update(trigger: data)
668
- end
669
- end
670
-
671
- # When a trigger is updated it could change items which modifies topics and
672
- # potentially adds or removes topics so refresh everything just to be safe
673
- def rebuild_trigger_event(data)
674
- @logger.debug "TriggerGroupMicroservice rebuild_trigger_event #{data}"
675
- if data['group'] == @group
676
- @share.trigger_base.update(trigger: data)
677
- @read_topic = false
678
- end
679
- end
680
-
681
- # Remove the trigger from the share.
682
- def deleted_trigger_event(data)
683
- @logger.debug "TriggerGroupMicroservice deleted_trigger_event #{data}"
684
- if data['group'] == @group
685
- @share.trigger_base.remove(trigger: data)
686
- @manager.refresh()
687
- end
688
- end
689
-
690
- def shutdown
691
- @read_topic = false
692
- @manager.shutdown()
693
- super
694
- end
695
- end
696
- end
697
-
698
- OpenC3::TriggerGroupMicroservice.run if __FILE__ == $0