openc3 5.8.1 → 5.9.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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/ext/openc3/ext/crc/crc.c +1 -1
  3. data/lib/openc3/api/cmd_api.rb +1 -1
  4. data/lib/openc3/microservices/decom_microservice.rb +10 -2
  5. data/lib/openc3/microservices/reaction_microservice.rb +152 -81
  6. data/lib/openc3/microservices/timeline_microservice.rb +1 -1
  7. data/lib/openc3/microservices/trigger_group_microservice.rb +188 -118
  8. data/lib/openc3/migrations/20230615000000_autonomic.rb +86 -0
  9. data/lib/openc3/models/activity_model.rb +2 -4
  10. data/lib/openc3/models/microservice_model.rb +6 -2
  11. data/lib/openc3/models/model.rb +1 -3
  12. data/lib/openc3/models/reaction_model.rb +124 -119
  13. data/lib/openc3/models/scope_model.rb +15 -3
  14. data/lib/openc3/models/timeline_model.rb +1 -3
  15. data/lib/openc3/models/trigger_group_model.rb +16 -50
  16. data/lib/openc3/models/trigger_model.rb +86 -123
  17. data/lib/openc3/packets/json_packet.rb +2 -3
  18. data/lib/openc3/script/commands.rb +10 -0
  19. data/lib/openc3/script/script.rb +1 -0
  20. data/lib/openc3/top_level.rb +0 -12
  21. data/lib/openc3/utilities/authorization.rb +1 -1
  22. data/lib/openc3/utilities/bucket_require.rb +5 -1
  23. data/lib/openc3/utilities/bucket_utilities.rb +4 -1
  24. data/lib/openc3/utilities/cli_generator.rb +56 -4
  25. data/lib/openc3/utilities/ruby_lex_utils.rb +4 -0
  26. data/lib/openc3/version.rb +6 -6
  27. data/templates/plugin/README.md +54 -4
  28. data/templates/plugin/Rakefile +31 -3
  29. data/templates/tool_angular/.editorconfig +16 -0
  30. data/templates/tool_angular/.gitignore +44 -0
  31. data/templates/tool_angular/.vscode/extensions.json +4 -0
  32. data/templates/tool_angular/.vscode/launch.json +20 -0
  33. data/templates/tool_angular/.vscode/tasks.json +42 -0
  34. data/templates/tool_angular/angular.json +111 -0
  35. data/templates/tool_angular/extra-webpack.config.js +8 -0
  36. data/templates/tool_angular/package.json +47 -0
  37. data/templates/tool_angular/src/app/app-routing.module.ts +15 -0
  38. data/templates/tool_angular/src/app/app.component.html +31 -0
  39. data/templates/tool_angular/src/app/app.component.scss +26 -0
  40. data/templates/tool_angular/src/app/app.component.spec.ts +29 -0
  41. data/templates/tool_angular/src/app/app.component.ts +51 -0
  42. data/templates/tool_angular/src/app/app.module.ts +30 -0
  43. data/templates/tool_angular/src/app/custom-overlay-container.ts +17 -0
  44. data/templates/tool_angular/src/app/empty-route/empty-route.component.ts +7 -0
  45. data/templates/tool_angular/src/app/openc3-api.d.ts +1 -0
  46. data/templates/tool_angular/src/assets/.gitkeep +0 -0
  47. data/templates/tool_angular/src/environments/environment.prod.ts +3 -0
  48. data/templates/tool_angular/src/environments/environment.ts +16 -0
  49. data/templates/tool_angular/src/favicon.ico +0 -0
  50. data/templates/tool_angular/src/index.html +13 -0
  51. data/templates/tool_angular/src/main.single-spa.ts +40 -0
  52. data/templates/tool_angular/src/single-spa/asset-url.ts +12 -0
  53. data/templates/tool_angular/src/single-spa/single-spa-props.ts +8 -0
  54. data/templates/tool_angular/src/styles.scss +1 -0
  55. data/templates/tool_angular/tsconfig.app.json +13 -0
  56. data/templates/tool_angular/tsconfig.json +33 -0
  57. data/templates/tool_angular/tsconfig.spec.json +14 -0
  58. data/templates/tool_angular/yarn.lock +8080 -0
  59. data/templates/tool_react/.eslintrc +7 -0
  60. data/templates/tool_react/.gitignore +72 -0
  61. data/templates/tool_react/.prettierignore +8 -0
  62. data/templates/tool_react/babel.config.json +29 -0
  63. data/templates/tool_react/jest.config.js +12 -0
  64. data/templates/tool_react/package.json +53 -0
  65. data/templates/tool_react/src/openc3-tool_name.js +24 -0
  66. data/templates/tool_react/src/root.component.js +88 -0
  67. data/templates/tool_react/src/root.component.test.js +9 -0
  68. data/templates/tool_react/webpack.config.js +27 -0
  69. data/templates/tool_react/yarn.lock +6854 -0
  70. data/templates/tool_svelte/.gitignore +72 -0
  71. data/templates/tool_svelte/.prettierignore +8 -0
  72. data/templates/tool_svelte/babel.config.js +12 -0
  73. data/templates/tool_svelte/build/smui.css +5 -0
  74. data/templates/tool_svelte/jest.config.js +9 -0
  75. data/templates/tool_svelte/package.json +46 -0
  76. data/templates/tool_svelte/rollup.config.js +72 -0
  77. data/templates/tool_svelte/src/App.svelte +42 -0
  78. data/templates/tool_svelte/src/App.test.js +9 -0
  79. data/templates/tool_svelte/src/services/api.js +92 -0
  80. data/templates/tool_svelte/src/services/axios.js +85 -0
  81. data/templates/tool_svelte/src/services/cable.js +65 -0
  82. data/templates/tool_svelte/src/services/config-parser.js +199 -0
  83. data/templates/tool_svelte/src/services/openc3-api.js +647 -0
  84. data/templates/tool_svelte/src/theme/_smui-theme.scss +25 -0
  85. data/templates/tool_svelte/src/tool_name.js +17 -0
  86. data/templates/tool_svelte/yarn.lock +5052 -0
  87. data/templates/tool_vue/.browserslistrc +16 -0
  88. data/templates/tool_vue/.env.standalone +1 -0
  89. data/templates/tool_vue/.eslintrc.js +43 -0
  90. data/templates/tool_vue/.gitignore +2 -0
  91. data/templates/tool_vue/.nycrc +3 -0
  92. data/templates/tool_vue/.prettierrc.js +5 -0
  93. data/templates/tool_vue/babel.config.json +11 -0
  94. data/templates/tool_vue/jsconfig.json +6 -0
  95. data/templates/tool_vue/package.json +52 -0
  96. data/templates/tool_vue/src/App.vue +15 -0
  97. data/templates/tool_vue/src/main.js +38 -0
  98. data/templates/tool_vue/src/router.js +29 -0
  99. data/templates/tool_vue/src/tools/tool_name/tool_name.vue +63 -0
  100. data/templates/tool_vue/vue.config.js +30 -0
  101. data/templates/tool_vue/yarn.lock +9145 -0
  102. data/templates/widget/package.json +9 -9
  103. data/templates/widget/yarn.lock +77 -73
  104. metadata +76 -2
@@ -25,40 +25,47 @@ require 'openc3/models/notification_model'
25
25
  require 'openc3/models/trigger_model'
26
26
  require 'openc3/topics/autonomic_topic'
27
27
  require 'openc3/utilities/authentication'
28
+ require 'openc3/packets/json_packet'
28
29
 
29
30
  require 'openc3/script'
30
31
 
31
32
  module OpenC3
32
-
33
33
  class TriggerLoopError < TriggerError; end
34
34
 
35
35
  # Stored in the TriggerGroupShare this should be a thread safe
36
36
  # hash that triggers will be added, updated, and removed from
37
37
  class PacketBase
38
-
39
38
  def initialize(scope:)
40
39
  @scope = scope
41
40
  @mutex = Mutex.new
42
41
  @packets = Hash.new
43
42
  end
44
43
 
45
- # ["#{@scope}__DECOM__{#{@target}}__#{@packet}"]
46
44
  def packet(target:, packet:)
47
45
  topic = "#{@scope}__DECOM__{#{target}}__#{packet}"
48
46
  @mutex.synchronize do
49
- return Marshal.load( Marshal.dump(@packets[topic]) )
47
+ return nil unless @packets[topic]
48
+ # Deep copy the packet so it doesn't change under us
49
+ return Marshal.load( Marshal.dump(@packets[topic][-1]) )
50
50
  end
51
51
  end
52
52
 
53
- def get(topic:)
53
+ def previous_packet(target:, packet:)
54
+ topic = "#{@scope}__DECOM__{#{target}}__#{packet}"
54
55
  @mutex.synchronize do
55
- return Marshal.load( Marshal.dump(@packets[topic]) )
56
+ return nil unless @packets[topic] and @packets[topic].length == 2
57
+ # Deep copy the packet so it doesn't change under us
58
+ return Marshal.load( Marshal.dump(@packets[topic][0]) )
56
59
  end
57
60
  end
58
61
 
59
62
  def add(topic:, packet:)
60
63
  @mutex.synchronize do
61
- @packets[topic] = packet
64
+ @packets[topic] ||= []
65
+ if @packets[topic].length == 2
66
+ @packets[topic].shift
67
+ end
68
+ @packets[topic].push(packet)
62
69
  end
63
70
  end
64
71
 
@@ -72,8 +79,7 @@ module OpenC3
72
79
  # Stored in the TriggerGroupShare this should be a thread safe
73
80
  # hash that triggers will be added, updated, and removed from.
74
81
  class TriggerBase
75
-
76
- attr_reader :autonomic_topic
82
+ attr_reader :autonomic_topic, :triggers
77
83
 
78
84
  def initialize(scope:)
79
85
  @scope = scope
@@ -84,8 +90,8 @@ module OpenC3
84
90
  @lookup = Hash.new
85
91
  end
86
92
 
87
- # Get triggers to evaluate based on the topic. IF the
88
- # topic is the equal to the autonomic topic it will
93
+ # Get triggers to evaluate based on the topic. If the
94
+ # topic is equal to the autonomic topic it will
89
95
  # return only triggers with roots
90
96
  def get_triggers(topic:)
91
97
  if @autonomic_topic == topic
@@ -104,19 +110,19 @@ module OpenC3
104
110
  data = @triggers[name]
105
111
  return unless data
106
112
  trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
107
- if value == -1 && trigger.active
108
- trigger.deactivate()
113
+ if value == -1 && trigger.enabled
114
+ trigger.disable()
109
115
  elsif value == 1 && trigger.state == false
110
- trigger.enable()
116
+ trigger.state = true
111
117
  elsif value == 0 && trigger.state == true
112
- trigger.disable()
118
+ trigger.state = false
113
119
  end
114
120
  @triggers[name] = trigger.as_json(:allow_nan => true)
115
121
  end
116
122
  end
117
123
 
118
- # returns a Hash of ALL active Trigger objects
119
- def triggers
124
+ # returns a Hash of ALL enabled Trigger objects
125
+ def enabled_triggers
120
126
  val = nil
121
127
  @triggers_mutex.synchronize do
122
128
  val = Marshal.load( Marshal.dump(@triggers) )
@@ -124,12 +130,12 @@ module OpenC3
124
130
  ret = Hash.new
125
131
  val.each do | name, data |
126
132
  trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
127
- ret[name] = trigger if trigger.active
133
+ ret[name] = trigger if trigger.enabled
128
134
  end
129
135
  return ret
130
136
  end
131
137
 
132
- # returns an Array of active Trigger objects that have roots to other triggers
138
+ # returns an Array of enabled Trigger objects that have roots to other triggers
133
139
  def triggers_with_roots
134
140
  val = nil
135
141
  @triggers_mutex.synchronize do
@@ -138,12 +144,12 @@ module OpenC3
138
144
  ret = []
139
145
  val.each do | _name, data |
140
146
  trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
141
- ret << trigger if trigger.active && ! trigger.roots.empty?
147
+ ret << trigger if trigger.enabled && ! trigger.roots.empty?
142
148
  end
143
149
  return ret
144
150
  end
145
151
 
146
- # returns an Array of active Trigger objects that use a topic
152
+ # returns an Array of enabled Trigger objects that use a topic
147
153
  def triggers_from(topic:)
148
154
  val = nil
149
155
  @lookup_mutex.synchronize do
@@ -152,10 +158,10 @@ module OpenC3
152
158
  return [] if val.nil?
153
159
  ret = []
154
160
  @triggers_mutex.synchronize do
155
- val.each do | trigger_name, _v |
161
+ val.each do | trigger_name |
156
162
  data = Marshal.load( Marshal.dump(@triggers[trigger_name]) )
157
163
  trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
158
- ret << trigger if trigger.active
164
+ ret << trigger if trigger.enabled
159
165
  end
160
166
  end
161
167
  return ret
@@ -168,53 +174,60 @@ module OpenC3
168
174
  end
169
175
  end
170
176
 
171
- # database update of all triggers in the group
172
- def update(triggers:)
177
+ # Rebuild the database lookup of all triggers in the group
178
+ def rebuild(triggers:)
173
179
  @triggers_mutex.synchronize do
174
180
  @triggers = Marshal.load( Marshal.dump(triggers) )
175
181
  end
176
182
  @lookup_mutex.synchronize do
177
- @lookup = {@autonomic_topic => {}}
183
+ @lookup = { @autonomic_topic => [] }
178
184
  triggers.each do | _name, data |
179
185
  trigger = TriggerModel.from_json(data, name: data['name'], scope: data['scope'])
180
186
  trigger.generate_topics.each do | topic |
181
- if @lookup[topic].nil?
182
- @lookup[topic] = { trigger.name => 1 }
183
- else
184
- @lookup[topic][trigger.name] = 1
185
- end
187
+ @lookup[topic] ||= []
188
+ @lookup[topic] << trigger.name
186
189
  end
187
190
  end
188
191
  end
189
192
  end
190
193
 
191
- # add a trigger from TriggerBase
194
+ # Add a trigger from TriggerBase, must only be called once per trigger
192
195
  def add(trigger:)
193
196
  @triggers_mutex.synchronize do
194
197
  @triggers[trigger['name']] = Marshal.load( Marshal.dump(trigger) )
195
198
  end
196
- t = TriggerModel.from_json(trigger, name: trigger['name'], scope: trigger['scope'])
199
+ trigger = TriggerModel.from_json(trigger, name: trigger['name'], scope: trigger['scope'])
197
200
  @lookup_mutex.synchronize do
198
- t.generate_topics.each do | topic |
199
- if @lookup[topic].nil?
200
- @lookup[topic] = { t.name => 1 }
201
- else
202
- @lookup[topic][t.name] = 1
203
- end
201
+ trigger.generate_topics.each do | topic |
202
+ @lookup[topic] ||= []
203
+ @lookup[topic] << trigger.name
204
204
  end
205
205
  end
206
206
  end
207
207
 
208
+ # update a trigger from TriggerBase
209
+ def update(trigger:)
210
+ @triggers_mutex.synchronize do
211
+ model = TriggerModel.from_json(trigger, name: trigger['name'], scope: trigger['scope'])
212
+ model.update()
213
+ @triggers[trigger['name']] = model.as_json(:allow_nan => true)
214
+ end
215
+ end
216
+
208
217
  # remove a trigger from TriggerBase
209
218
  def remove(trigger:)
219
+ topics = []
210
220
  @triggers_mutex.synchronize do
211
221
  @triggers.delete(trigger['name'])
222
+ model = TriggerModel.from_json(trigger, name: trigger['name'], scope: trigger['scope'])
223
+ topics = model.generate_topics()
224
+ TriggerModel.delete(name: trigger['name'], group: trigger['group'], scope: trigger['scope'])
212
225
  end
213
- t = TriggerModel.from_json(trigger, name: trigger['name'], scope: trigger['scope'])
214
226
  @lookup_mutex.synchronize do
215
- t.generate_topics.each do | topic |
227
+ topics.each do | topic |
216
228
  unless @lookup[topic].nil?
217
- @lookup[topic].delete(t.name)
229
+ @lookup[topic].delete(trigger['name'])
230
+ @lookup.delete(topic) if @lookup[topic].empty?
218
231
  end
219
232
  end
220
233
  end
@@ -223,13 +236,8 @@ module OpenC3
223
236
 
224
237
  # Shared between the monitor thread and the manager thread to
225
238
  # share the triggers. This should remain a thread
226
- # safe implamentation.
239
+ # safe implementation.
227
240
  class TriggerGroupShare
228
-
229
- def self.get_group(name:)
230
- return name.split('__')[2]
231
- end
232
-
233
241
  attr_reader :trigger_base, :packet_base
234
242
 
235
243
  def initialize(scope:)
@@ -241,16 +249,16 @@ module OpenC3
241
249
 
242
250
  # The TriggerGroupWorker is a very simple thread pool worker. Once
243
251
  # the trigger manager has pushed a packet to the queue one of
244
- # these workers will evaluate the triggers in the kit and
245
- # evaluate triggers for that packet.
252
+ # these workers will evaluate the triggers for that packet.
246
253
  class TriggerGroupWorker
247
254
  TYPE = 'type'.freeze
248
- ITEM_RAW = 'raw'.freeze
249
255
  ITEM_TARGET = 'target'.freeze
250
256
  ITEM_PACKET = 'packet'.freeze
251
257
  ITEM_TYPE = 'item'.freeze
258
+ ITEM_VALUE_TYPE = 'valueType'.freeze
252
259
  FLOAT_TYPE = 'float'.freeze
253
260
  STRING_TYPE = 'string'.freeze
261
+ REGEX_TYPE = 'regex'.freeze
254
262
  LIMIT_TYPE = 'limit'.freeze
255
263
  TRIGGER_TYPE = 'trigger'.freeze
256
264
 
@@ -266,13 +274,29 @@ module OpenC3
266
274
  @ident = ident
267
275
  end
268
276
 
277
+ def notify(name:, severity:, message:)
278
+ data = {}
279
+ # All AutonomicTopic notifications must have 'name' and 'updated_at' in the data
280
+ data['name'] = name
281
+ data['updated_at'] = Time.now.to_nsec_from_epoch
282
+ data['severity'] = severity
283
+ data['message'] = message
284
+ notification = {
285
+ 'kind' => 'error',
286
+ 'type' => 'trigger',
287
+ 'data' => JSON.generate(data),
288
+ }
289
+ AutonomicTopic.write_notification(notification, scope: @scope)
290
+ @logger.public_send(severity.intern, message)
291
+ end
292
+
269
293
  def run
270
294
  @logger.info "TriggerGroupWorker-#{@ident} running"
271
295
  loop do
272
296
  topic = @queue.pop
273
297
  break if topic.nil?
274
298
  begin
275
- evaluate_wrapper(topic: topic)
299
+ evaluate_data_packet(topic: topic)
276
300
  rescue StandardError => e
277
301
  @logger.error "TriggerGroupWorker-#{@ident} failed to evaluate data packet from topic: #{topic}\n#{e.formatted}"
278
302
  end
@@ -280,24 +304,18 @@ module OpenC3
280
304
  @logger.info "TriggerGroupWorker-#{@ident} exiting"
281
305
  end
282
306
 
283
- def evaluate_wrapper(topic:)
284
- evaluate_data_packet(topic: topic, triggers: @share.trigger_base.triggers)
285
- end
286
-
287
307
  # Each packet will be evaluated to all triggers and use the result to send
288
308
  # the results back to the topic to be used by the reaction microservice.
289
- def evaluate_data_packet(topic:, triggers:)
309
+ def evaluate_data_packet(topic:)
290
310
  visited = Hash.new
291
311
  @logger.debug "TriggerGroupWorker-#{@ident} topic: #{topic}"
292
- triggers_to_eval = @share.trigger_base.get_triggers(topic: topic)
293
- @logger.debug "TriggerGroupWorker-#{@ident} triggers_to_eval: #{triggers_to_eval}"
294
- triggers_to_eval.each do | trigger |
312
+ @share.trigger_base.get_triggers(topic: topic).each do |trigger|
295
313
  @logger.debug "TriggerGroupWorker-#{@ident} eval head: #{trigger}"
296
314
  value = evaluate_trigger(
297
315
  head: trigger,
298
316
  trigger: trigger,
299
317
  visited: visited,
300
- triggers: triggers
318
+ triggers: @share.trigger_base.enabled_triggers
301
319
  )
302
320
  @logger.debug "TriggerGroupWorker-#{@ident} trigger: #{trigger} value: #{value}"
303
321
  # value MUST be -1, 0, or 1
@@ -315,36 +333,54 @@ module OpenC3
315
333
  packet: operand[ITEM_PACKET]
316
334
  )
317
335
  return nil if packet.nil?
318
- limit = packet["#{operand[ITEM_TYPE]}__L"]
319
- if limit.nil? == false && limit.include?('_')
320
- return other[LIMIT_TYPE] if limit.include?(other[LIMIT_TYPE])
321
- end
336
+ _, limit = packet.read_with_limits_state(operand[ITEM_TYPE], operand[ITEM_VALUE_TYPE].intern)
322
337
  return limit
323
338
  end
324
339
 
325
340
  # extract the value outlined in the operand to get the packet item value
326
341
  # IF raw in operand it will pull the raw value over the converted
327
- def get_packet_value(operand:)
328
- packet = @share.packet_base.packet(
329
- target: operand[ITEM_TARGET],
330
- packet: operand[ITEM_PACKET]
331
- )
332
- return nil if packet.nil?
333
-
334
- value_type = operand[ITEM_RAW] ? '' : '__C'
335
- return packet["#{operand[ITEM_TYPE]}#{value_type}"]
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
+ raise "Packet #{operand[ITEM_TARGET]} #{operand[ITEM_PACKET]} not found" if packet.nil?
359
+ value = packet.read(operand[ITEM_TYPE], operand[ITEM_VALUE_TYPE].intern)
360
+ raise "Item #{operand[ITEM_TARGET]} #{operand[ITEM_PACKET]} #{operand[ITEM_TYPE]} not found" if value.nil?
361
+ value
336
362
  end
337
363
 
338
364
  # extract the value of the operand from the packet
339
- def operand_value(operand:, other:, visited:)
340
- if operand[TYPE] == ITEM_TYPE && other[TYPE] == LIMIT_TYPE
365
+ def operand_value(operand:, other:, visited:, previous: false)
366
+ if operand[TYPE] == ITEM_TYPE && other && other[TYPE] == LIMIT_TYPE
341
367
  return get_packet_limit(operand: operand, other: other)
342
368
  elsif operand[TYPE] == ITEM_TYPE
343
- return get_packet_value(operand: operand)
369
+ return get_packet_value(operand: operand, previous: previous)
344
370
  elsif operand[TYPE] == TRIGGER_TYPE
345
371
  return visited["#{operand[TRIGGER_TYPE]}__R"] == 1
346
- else
372
+ elsif operand[TYPE] == FLOAT_TYPE
373
+ return operand[operand[TYPE]].to_f
374
+ elsif operand[TYPE] == STRING_TYPE
375
+ return operand[operand[TYPE]].to_s
376
+ elsif operand[TYPE] == REGEX_TYPE
377
+ # This can potentially throw an exception on badly formatted Regexp
378
+ return Regexp.new(operand[operand[TYPE]])
379
+ elsif operand[TYPE] == LIMIT_TYPE
347
380
  return operand[operand[TYPE]]
381
+ else
382
+ # This is a logic error ... should never get here
383
+ raise "Unknown operand type: #{operand}"
348
384
  end
349
385
  end
350
386
 
@@ -353,8 +389,8 @@ module OpenC3
353
389
  # 0 (the value is considered as a false value)
354
390
  # 1 (the value is considered as a true value)
355
391
  #
356
- def evaluate(left:, operator:, right:)
357
- @logger.debug "TriggerGroupWorker-#{@ident} evaluate: (#{left} #{operator} #{right})"
392
+ def evaluate(name:, left:, operator:, right:)
393
+ @logger.debug "TriggerGroupWorker-#{@ident} evaluate: (#{left}(#{left.class}) #{operator} #{right}(#{right.class}))"
358
394
  begin
359
395
  case operator
360
396
  when '>'
@@ -365,17 +401,22 @@ module OpenC3
365
401
  return left >= right ? 1 : 0
366
402
  when '<='
367
403
  return left <= right ? 1 : 0
368
- when '!='
404
+ when '!=', 'CHANGES'
369
405
  return left != right ? 1 : 0
370
- when '=='
406
+ when '==', 'DOES NOT CHANGE'
371
407
  return left == right ? 1 : 0
408
+ when '!~'
409
+ return left !~ right ? 1 : 0
410
+ when '=~'
411
+ return left =~ right ? 1 : 0
372
412
  when 'AND'
373
413
  return left && right ? 1 : 0
374
414
  when 'OR'
375
415
  return left || right ? 1 : 0
376
416
  end
377
- rescue ArgumentError
378
- @logger.error "invalid evaluate: (#{left} #{operator} #{right})"
417
+ rescue ArgumentError => error
418
+ message = "invalid evaluate: (#{left} #{operator} #{right})"
419
+ notify(name: name, severity: 'error', message: message)
379
420
  return -1
380
421
  end
381
422
  end
@@ -384,7 +425,7 @@ module OpenC3
384
425
  # TriggerGroupWorkers to call. It will use the trigger name and append a
385
426
  # __P for path or __R for result. The Path is a Hash that contains
386
427
  # a key for each node traveled to get results. When the result has
387
- # been found it will be stored in the result key __R in the vistied Hash
428
+ # been found it will be stored in the result key __R in the visited Hash
388
429
  # and eval_trigger will return a number.
389
430
  # -1 (the value is considered an error used to disable the trigger)
390
431
  # 0 (the value is considered as a false value)
@@ -401,14 +442,16 @@ module OpenC3
401
442
  end
402
443
  if visited["#{head.name}__P"][trigger.name]
403
444
  # Not sure if this is posible as on create it validates that the dependents are already created
404
- @logger.error "loop detected from #{head} -> #{trigger} path: #{visited["#{head.name}__P"]}"
445
+ message = "loop detected from #{head.name} -> #{trigger.name} path: #{visited["#{head.name}__P"]}"
446
+ notify(name: trigger.name, severity: 'error', message: error.message)
405
447
  return visited["#{trigger.name}__R"] = -1
406
448
  end
407
449
  trigger.roots.each do | root_trigger_name |
408
450
  next if visited["#{root_trigger_name}__R"]
409
451
  root_trigger = triggers[root_trigger_name]
410
452
  if head.name == root_trigger.name
411
- @logger.error "loop detected from #{head} -> #{root_trigger} path: #{visited["#{head.name}__P"]}"
453
+ message = "loop detected from #{head.name} -> #{root_trigger_name} path: #{visited["#{head.name}__P"]}"
454
+ notify(name: trigger.name, severity: 'error', message: error.message)
412
455
  return visited["#{trigger.name}__R"] = -1
413
456
  end
414
457
  result = evaluate_trigger(
@@ -420,23 +463,36 @@ module OpenC3
420
463
  @logger.debug "TriggerGroupWorker-#{@ident} #{root_trigger.name} result: #{result}"
421
464
  visited["#{root_trigger.name}__R"] = visited["#{head.name}__P"][root_trigger.name] = result
422
465
  end
423
- left = operand_value(operand: trigger.left, other: trigger.right, visited: visited)
424
- right = operand_value(operand: trigger.right, other: trigger.left, visited: visited)
466
+ begin
467
+ left = operand_value(operand: trigger.left, other: trigger.right, visited: visited)
468
+ if trigger.operator.include?('CHANGE')
469
+ right = operand_value(operand: trigger.left, other: trigger.right, visited: visited, previous: true)
470
+ else
471
+ right = operand_value(operand: trigger.right, other: trigger.left, visited: visited)
472
+ end
473
+ rescue => error
474
+ # This will primarily happen when the user inputs a bad Regexp
475
+ notify(name: trigger.name, severity: 'error', message: error.message)
476
+ return visited["#{trigger.name}__R"] = -1
477
+ end
478
+ # Convert the standard '==' and '!=' into Ruby Regexp operators
479
+ operator = trigger.operator
480
+ if right and right.is_a? Regexp
481
+ operator = '=~' if operator == '=='
482
+ operator = '!~' if operator == '!='
483
+ end
425
484
  if left.nil? || right.nil?
426
485
  return visited["#{trigger.name}__R"] = 0
427
486
  end
428
- result = evaluate(left: left, operator: trigger.operator, right: right)
487
+ result = evaluate(name: trigger.name,left: left, operator: operator, right: right)
429
488
  return visited["#{trigger.name}__R"] = result
430
489
  end
431
-
432
490
  end
433
491
 
434
492
  # The trigger manager starts a thread pool and subscribes
435
- # to the telemtry decom topic add the packet to a queue.
436
- # TriggerGroupManager adds the "packet" to the thread pool queue
493
+ # to the telemtry decom topic. It adds the "packet" to the thread pool queue
437
494
  # and the thread will evaluate the "trigger".
438
495
  class TriggerGroupManager
439
-
440
496
  attr_reader :name, :scope, :share, :group, :topics, :thread_pool
441
497
 
442
498
  def initialize(name:, logger:, scope:, group:, share:)
@@ -480,7 +536,6 @@ module OpenC3
480
536
  @logger.error "TriggerGroupManager failed to update topics.\n#{e.formatted}"
481
537
  end
482
538
  break if @cancel_thread
483
-
484
539
  block_for_updates()
485
540
  break if @cancel_thread
486
541
  end
@@ -503,7 +558,7 @@ module OpenC3
503
558
  Topic.read_topics(@topics) do |topic, _msg_id, msg_hash, _redis|
504
559
  @logger.debug "TriggerGroupManager block_for_updates: #{topic} #{msg_hash.to_s}"
505
560
  if topic != @share.trigger_base.autonomic_topic
506
- packet = JSON.parse(msg_hash['json_data'], :allow_nan => true, :create_additions => true)
561
+ packet = JsonPacket.new(:TLM, msg_hash['target_name'], msg_hash['packet_name'], msg_hash['time'].to_i, false, msg_hash["json_data"])
507
562
  @share.packet_base.add(topic: topic, packet: packet)
508
563
  end
509
564
  @queue << "#{topic}"
@@ -533,10 +588,23 @@ module OpenC3
533
588
  # stream this will trigger an update again to the schedule.
534
589
  class TriggerGroupMicroservice < Microservice
535
590
  attr_reader :name, :scope, :share, :group, :manager, :manager_thread
591
+ # This lookup is mapping all the different trigger notifications
592
+ # which are primarily sent by notify in TriggerModel
593
+ TOPIC_LOOKUP = {
594
+ 'error' => :no_op, # Sent by TriggerGroupWorker
595
+ 'created' => :created_trigger_event,
596
+ 'updated' => :rebuild_trigger_event,
597
+ 'deleted' => :deleted_trigger_event,
598
+ 'enabled' => :updated_trigger_event,
599
+ 'disabled' => :updated_trigger_event,
600
+ 'true' => :no_op, # Sent by TriggerGroupWorker
601
+ 'false' => :no_op, # Sent by TriggerGroupWorker
602
+ }
536
603
 
537
604
  def initialize(*args)
538
605
  super(*args)
539
- @group = TriggerGroupShare.get_group(name: @name)
606
+ # The name is passed in via the trigger_group_model as "#{scope}__TRIGGER_GROUP__#{name}"
607
+ @group = @name.split('__')[2]
540
608
  @share = TriggerGroupShare.new(scope: @scope)
541
609
  @manager = TriggerGroupManager.new(name: @name, logger: @logger, scope: @scope, group: @group, share: @share)
542
610
  @manager_thread = nil
@@ -548,36 +616,26 @@ module OpenC3
548
616
  @manager_thread = Thread.new { @manager.run }
549
617
  loop do
550
618
  triggers = TriggerModel.all(scope: @scope, group: @group)
551
- @share.trigger_base.update(triggers: triggers)
619
+ @share.trigger_base.rebuild(triggers: triggers)
620
+ @manager.refresh() # Everytime we do a full base update we refesh the manager
552
621
  break if @cancel_thread
553
-
554
622
  block_for_updates()
555
623
  break if @cancel_thread
556
624
  end
557
625
  @logger.info "TriggerGroupMicroservice exiting"
558
626
  end
559
627
 
560
- def topic_lookup_functions
561
- return {
562
- 'created' => :created_trigger_event,
563
- 'updated' => :created_trigger_event,
564
- 'deleted' => :deleted_trigger_event,
565
- 'enabled' => :created_trigger_event,
566
- 'disabled' => :created_trigger_event,
567
- 'activated' => :created_trigger_event,
568
- 'deactivated' => :created_trigger_event,
569
- }
570
- end
571
-
572
628
  def block_for_updates
573
629
  @read_topic = true
574
- while @read_topic
630
+ while @read_topic && !@cancel_thread
575
631
  begin
576
632
  AutonomicTopic.read_topics(@topics) do |_topic, _msg_id, msg_hash, _redis|
633
+ break if @cancel_thread
577
634
  @logger.debug "TriggerGroupMicroservice block_for_updates: #{msg_hash.to_s}"
635
+ # Process trigger notifications created by TriggerModel notify
578
636
  if msg_hash['type'] == 'trigger'
579
637
  data = JSON.parse(msg_hash['data'], :allow_nan => true, :create_additions => true)
580
- public_send(topic_lookup_functions[msg_hash['kind']], data)
638
+ public_send(TOPIC_LOOKUP[msg_hash['kind']], data)
581
639
  end
582
640
  end
583
641
  rescue StandardError => e
@@ -590,11 +648,6 @@ module OpenC3
590
648
  @logger.debug "TriggerGroupMicroservice web socket event: #{data}"
591
649
  end
592
650
 
593
- def refresh_event(data)
594
- @logger.debug "TriggerGroupMicroservice web socket schedule refresh: #{data}"
595
- @read_topic = false
596
- end
597
-
598
651
  # Add the trigger to the share.
599
652
  def created_trigger_event(data)
600
653
  @logger.debug "TriggerGroupMicroservice created_trigger_event #{data}"
@@ -604,6 +657,23 @@ module OpenC3
604
657
  end
605
658
  end
606
659
 
660
+ def updated_trigger_event(data)
661
+ @logger.debug "TriggerGroupMicroservice updated_trigger_event #{data}"
662
+ if data['group'] == @group
663
+ @share.trigger_base.update(trigger: data)
664
+ end
665
+ end
666
+
667
+ # When a trigger is updated it could change items which modifies topics and
668
+ # potentially adds or removes topics so refresh everything just to be safe
669
+ def rebuild_trigger_event(data)
670
+ @logger.debug "TriggerGroupMicroservice rebuild_trigger_event #{data}"
671
+ if data['group'] == @group
672
+ @share.trigger_base.update(trigger: data)
673
+ @read_topic = false
674
+ end
675
+ end
676
+
607
677
  # Remove the trigger from the share.
608
678
  def deleted_trigger_event(data)
609
679
  @logger.debug "TriggerGroupMicroservice deleted_trigger_event #{data}"