openc3 7.0.0 → 7.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +105 -13
  3. data/bin/pipinstall +38 -6
  4. data/data/config/command_modifiers.yaml +1 -0
  5. data/data/config/item_modifiers.yaml +2 -1
  6. data/data/config/microservice.yaml +12 -1
  7. data/data/config/parameter_modifiers.yaml +49 -7
  8. data/data/config/table_parameter_modifiers.yaml +3 -1
  9. data/data/config/target.yaml +11 -0
  10. data/data/config/target_config.yaml +6 -2
  11. data/lib/openc3/accessors/template_accessor.rb +9 -0
  12. data/lib/openc3/api/cmd_api.rb +2 -1
  13. data/lib/openc3/api/metrics_api.rb +11 -1
  14. data/lib/openc3/api/tlm_api.rb +21 -6
  15. data/lib/openc3/core_ext/faraday.rb +1 -1
  16. data/lib/openc3/interfaces/interface.rb +1 -6
  17. data/lib/openc3/io/json_api.rb +1 -1
  18. data/lib/openc3/logs/log_writer.rb +3 -1
  19. data/lib/openc3/microservices/decom_common.rb +128 -0
  20. data/lib/openc3/microservices/decom_microservice.rb +27 -96
  21. data/lib/openc3/microservices/interface_decom_common.rb +28 -10
  22. data/lib/openc3/microservices/interface_microservice.rb +16 -9
  23. data/lib/openc3/microservices/log_microservice.rb +1 -1
  24. data/lib/openc3/microservices/microservice.rb +3 -2
  25. data/lib/openc3/microservices/queue_microservice.rb +1 -1
  26. data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
  27. data/lib/openc3/microservices/text_log_microservice.rb +1 -2
  28. data/lib/openc3/models/cvt_model.rb +24 -13
  29. data/lib/openc3/models/db_sharded_model.rb +110 -0
  30. data/lib/openc3/models/interface_model.rb +9 -0
  31. data/lib/openc3/models/interface_status_model.rb +33 -3
  32. data/lib/openc3/models/metric_model.rb +96 -37
  33. data/lib/openc3/models/microservice_model.rb +7 -0
  34. data/lib/openc3/models/microservice_status_model.rb +30 -3
  35. data/lib/openc3/models/plugin_model.rb +9 -1
  36. data/lib/openc3/models/python_package_model.rb +1 -1
  37. data/lib/openc3/models/reaction_model.rb +27 -9
  38. data/lib/openc3/models/reingest_job_model.rb +153 -0
  39. data/lib/openc3/models/scope_model.rb +3 -2
  40. data/lib/openc3/models/script_status_model.rb +4 -20
  41. data/lib/openc3/models/target_model.rb +113 -100
  42. data/lib/openc3/models/trigger_model.rb +24 -7
  43. data/lib/openc3/packets/packet_config.rb +4 -1
  44. data/lib/openc3/script/api_shared.rb +39 -2
  45. data/lib/openc3/script/calendar.rb +32 -10
  46. data/lib/openc3/script/extract.rb +46 -13
  47. data/lib/openc3/script/script.rb +2 -2
  48. data/lib/openc3/script/script_runner.rb +4 -4
  49. data/lib/openc3/script/telemetry.rb +3 -3
  50. data/lib/openc3/script/web_socket_api.rb +29 -22
  51. data/lib/openc3/system/system.rb +20 -3
  52. data/lib/openc3/topics/command_decom_topic.rb +4 -2
  53. data/lib/openc3/topics/command_topic.rb +8 -5
  54. data/lib/openc3/topics/decom_interface_topic.rb +31 -11
  55. data/lib/openc3/topics/interface_topic.rb +88 -27
  56. data/lib/openc3/topics/limits_event_topic.rb +62 -41
  57. data/lib/openc3/topics/router_topic.rb +61 -21
  58. data/lib/openc3/topics/system_events_topic.rb +18 -1
  59. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -1
  60. data/lib/openc3/topics/telemetry_topic.rb +4 -2
  61. data/lib/openc3/topics/topic.rb +77 -5
  62. data/lib/openc3/utilities/aws_bucket.rb +2 -0
  63. data/lib/openc3/utilities/cli_generator.rb +3 -2
  64. data/lib/openc3/utilities/ctrf.rb +231 -0
  65. data/lib/openc3/utilities/metric.rb +15 -1
  66. data/lib/openc3/utilities/questdb_client.rb +177 -40
  67. data/lib/openc3/utilities/reingest_job.rb +377 -0
  68. data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
  69. data/lib/openc3/utilities/store_autoload.rb +78 -52
  70. data/lib/openc3/utilities/store_queued.rb +20 -12
  71. data/lib/openc3/version.rb +5 -5
  72. data/templates/plugin/plugin.gemspec +13 -1
  73. data/templates/tool_angular/package.json +2 -2
  74. data/templates/tool_react/package.json +1 -1
  75. data/templates/tool_svelte/package.json +1 -1
  76. data/templates/tool_vue/package.json +3 -4
  77. data/templates/tool_vue/src/router.js +2 -2
  78. data/templates/widget/package.json +2 -2
  79. metadata +8 -3
@@ -99,7 +99,7 @@ module OpenC3
99
99
  def self.destroy(name, scope:)
100
100
  package_name, version = self.extract_name_and_version(name)
101
101
  Logger.info "Uninstalling package: #{name}"
102
- pip_args = ["-y", package_name]
102
+ pip_args = [package_name]
103
103
  result = OpenC3::ProcessManager.instance.spawn(["/openc3/bin/pipuninstall"] + pip_args, "package_uninstall", name, Time.now + 3600.0, scope: scope)
104
104
  return result.name
105
105
  end
@@ -32,10 +32,12 @@ module OpenC3
32
32
  ACTION_TYPES = [SCRIPT_REACTION, COMMAND_REACTION, NOTIFY_REACTION]
33
33
 
34
34
  def self.create_unique_name(scope:)
35
- reaction_names = self.names(scope: scope) # comes back sorted
35
+ reaction_names = self.names(scope: scope)
36
36
  num = 1 # Users count with 1
37
- if reaction_names[-1]
38
- num = reaction_names[-1][5..-1].to_i + 1
37
+ unless reaction_names.empty?
38
+ # Extract numeric suffixes and find the max to avoid lexicographic sort issues
39
+ max_num = reaction_names.map { |name| name[5..-1].to_i }.max
40
+ num = max_num + 1
39
41
  end
40
42
  return "REACT#{num}"
41
43
  end
@@ -77,7 +79,7 @@ module OpenC3
77
79
  end
78
80
 
79
81
  attr_reader :name, :scope, :snooze, :triggers, :actions, :enabled, :trigger_level, :snoozed_until
80
- attr_accessor :username, :shard
82
+ attr_accessor :username, :shard, :label
81
83
 
82
84
  def initialize(
83
85
  name:,
@@ -90,6 +92,7 @@ module OpenC3
90
92
  snoozed_until: nil,
91
93
  username: nil,
92
94
  shard: 0,
95
+ label: nil,
93
96
  updated_at: nil
94
97
  )
95
98
  super("#{scope}#{PRIMARY_KEY}", name: name, scope: scope)
@@ -102,6 +105,7 @@ module OpenC3
102
105
  @triggers = validate_triggers(triggers)
103
106
  @username = username
104
107
  @shard = shard.to_i # to_i to handle nil
108
+ @label = label
105
109
  @updated_at = updated_at
106
110
  end
107
111
 
@@ -177,28 +181,40 @@ module OpenC3
177
181
  return actions
178
182
  end
179
183
 
180
- def verify_triggers
184
+ # Validate that all triggers exist, but do not persist dependent changes yet.
185
+ # Returns the list of trigger models that need updating.
186
+ def validate_triggers_exist
181
187
  if @triggers.empty?
182
188
  raise ReactionInputError.new "reaction must contain at least one valid trigger: #{@triggers}"
183
189
  end
184
190
 
191
+ models_to_update = []
185
192
  @triggers.each do | trigger |
186
193
  model = TriggerModel.get(name: trigger['name'], group: trigger['group'], scope: @scope)
187
194
  if model.nil?
188
195
  raise ReactionInputError.new "failed to find trigger: #{trigger}"
189
196
  end
190
- model.update_dependents(dependent: @name)
191
- model.update()
197
+ unless model.dependents.include?(@name)
198
+ model.update_dependents(dependent: @name)
199
+ models_to_update << model
200
+ end
192
201
  end
202
+ models_to_update
203
+ end
204
+
205
+ # Persist dependent changes to trigger models
206
+ def commit_trigger_dependents(models)
207
+ models.each { |model| model.update() }
193
208
  end
194
209
 
195
210
  def create
196
211
  unless Store.hget(@primary_key, @name).nil?
197
212
  raise ReactionInputError.new "existing reaction found: #{@name}"
198
213
  end
199
- verify_triggers()
214
+ models = validate_triggers_exist()
200
215
  @updated_at = Time.now.to_nsec_from_epoch
201
216
  Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
217
+ commit_trigger_dependents(models)
202
218
  notify(kind: 'created')
203
219
  end
204
220
 
@@ -222,9 +238,10 @@ module OpenC3
222
238
  end
223
239
  end
224
240
 
225
- verify_triggers()
241
+ models = validate_triggers_exist()
226
242
  @updated_at = Time.now.to_nsec_from_epoch
227
243
  Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
244
+ commit_trigger_dependents(models)
228
245
  # No notification as this is only called via reaction_controller which already notifies
229
246
  end
230
247
 
@@ -281,6 +298,7 @@ module OpenC3
281
298
  'actions' => @actions,
282
299
  'username' => @username,
283
300
  'shard' => @shard,
301
+ 'label' => @label,
284
302
  'updated_at' => @updated_at
285
303
  }
286
304
  end
@@ -0,0 +1,153 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2026 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is distributed in the hope that it will be useful,
7
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
8
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ # See LICENSE.md for more details.
10
+ #
11
+ # This file may also be used under the terms of a commercial license
12
+ # if purchased from OpenC3, Inc.
13
+
14
+ require 'openc3/models/model'
15
+
16
+ module OpenC3
17
+ # Tracks one run of OpenC3::ReingestJob. The job updates this record from a
18
+ # background thread; the storage_controller status endpoint reads it.
19
+ # `updated_at` doubles as the heartbeat — if a Running record hasn't been
20
+ # touched in STALE_THRESHOLD_SEC, the status endpoint surfaces it as 'Stale'.
21
+ class ReingestJobModel < Model
22
+ PRIMARY_KEY = 'openc3_reingest_job'
23
+ STALE_THRESHOLD_SEC = 60
24
+
25
+ STATES = %w[Queued Running Complete Crashed Stale].freeze
26
+ PHASES = %w[downloading enabling_dedup ingesting dedup_cooldown disabling_dedup].freeze
27
+
28
+ attr_accessor :state
29
+ attr_accessor :files
30
+ attr_accessor :bucket
31
+ attr_accessor :path
32
+ attr_accessor :table_names
33
+ attr_accessor :target_version
34
+ attr_accessor :versions_used
35
+ attr_accessor :warnings
36
+ attr_accessor :progress_phase
37
+ attr_accessor :progress_current
38
+ attr_accessor :progress_total
39
+ attr_accessor :packets_written
40
+ attr_accessor :dedup_enabled_by_us
41
+ attr_accessor :dedup_preexisting
42
+ attr_accessor :dedup_disabled_tables
43
+ attr_accessor :dedup_cooldown_seconds
44
+ attr_accessor :dedup_enabled_at
45
+ attr_accessor :dedup_disabled_at
46
+ attr_accessor :error
47
+ attr_accessor :started_at
48
+ attr_accessor :finished_at
49
+
50
+ def self.get(name:, scope:)
51
+ super("#{scope}__#{PRIMARY_KEY}", name: name)
52
+ end
53
+
54
+ def self.names(scope:)
55
+ super("#{scope}__#{PRIMARY_KEY}")
56
+ end
57
+
58
+ def self.all(scope:)
59
+ all = super("#{scope}__#{PRIMARY_KEY}")
60
+ all.sort_by { |_key, value| value['updated_at'] }.reverse
61
+ end
62
+
63
+ def initialize(
64
+ name:,
65
+ state: 'Queued',
66
+ files: [],
67
+ bucket: nil,
68
+ path: nil,
69
+ table_names: [],
70
+ target_version: 'as_logged',
71
+ versions_used: [],
72
+ warnings: [],
73
+ progress_phase: nil,
74
+ progress_current: 0,
75
+ progress_total: 0,
76
+ packets_written: 0,
77
+ dedup_enabled_by_us: [],
78
+ dedup_preexisting: [],
79
+ dedup_disabled_tables: [],
80
+ dedup_cooldown_seconds: 60,
81
+ dedup_enabled_at: nil,
82
+ dedup_disabled_at: nil,
83
+ error: nil,
84
+ started_at: nil,
85
+ finished_at: nil,
86
+ updated_at: nil,
87
+ plugin: nil,
88
+ scope:
89
+ )
90
+ super("#{scope}__#{PRIMARY_KEY}", name: name, updated_at: updated_at, plugin: plugin, scope: scope)
91
+ @state = state
92
+ @files = files
93
+ @bucket = bucket
94
+ @path = path
95
+ @table_names = table_names
96
+ @target_version = target_version
97
+ @versions_used = versions_used
98
+ @warnings = warnings
99
+ @progress_phase = progress_phase
100
+ @progress_current = progress_current
101
+ @progress_total = progress_total
102
+ @packets_written = packets_written
103
+ @dedup_enabled_by_us = dedup_enabled_by_us
104
+ @dedup_preexisting = dedup_preexisting
105
+ @dedup_disabled_tables = dedup_disabled_tables
106
+ @dedup_cooldown_seconds = dedup_cooldown_seconds
107
+ @dedup_enabled_at = dedup_enabled_at
108
+ @dedup_disabled_at = dedup_disabled_at
109
+ @error = error
110
+ @started_at = started_at
111
+ @finished_at = finished_at
112
+ end
113
+
114
+ # True if state is Running but the heartbeat (updated_at) is older than
115
+ # STALE_THRESHOLD_SEC. Callers should surface state as 'Stale' in that case.
116
+ def stale?
117
+ return false unless @state == 'Running'
118
+ return false unless @updated_at
119
+ age_nsec = Time.now.to_nsec_from_epoch - @updated_at.to_i
120
+ age_nsec > STALE_THRESHOLD_SEC * 1_000_000_000
121
+ end
122
+
123
+ def as_json(*_a)
124
+ {
125
+ 'name' => @name,
126
+ 'state' => stale? ? 'Stale' : @state,
127
+ 'files' => @files,
128
+ 'bucket' => @bucket,
129
+ 'path' => @path,
130
+ 'table_names' => @table_names,
131
+ 'target_version' => @target_version,
132
+ 'versions_used' => @versions_used,
133
+ 'warnings' => @warnings,
134
+ 'progress_phase' => @progress_phase,
135
+ 'progress_current' => @progress_current,
136
+ 'progress_total' => @progress_total,
137
+ 'packets_written' => @packets_written,
138
+ 'dedup_enabled_by_us' => @dedup_enabled_by_us,
139
+ 'dedup_preexisting' => @dedup_preexisting,
140
+ 'dedup_disabled_tables' => @dedup_disabled_tables,
141
+ 'dedup_cooldown_seconds' => @dedup_cooldown_seconds,
142
+ 'dedup_enabled_at' => @dedup_enabled_at,
143
+ 'dedup_disabled_at' => @dedup_disabled_at,
144
+ 'error' => @error,
145
+ 'started_at' => @started_at,
146
+ 'finished_at' => @finished_at,
147
+ 'updated_at' => @updated_at,
148
+ 'plugin' => @plugin,
149
+ 'scope' => @scope,
150
+ }
151
+ end
152
+ end
153
+ end
@@ -389,8 +389,9 @@ module OpenC3
389
389
  end
390
390
 
391
391
  # Delete the topics we created for the scope
392
- Topic.del("#{@scope}__COMMAND__{UNKNOWN}__UNKNOWN")
393
- Topic.del("#{@scope}__TELEMETRY__{UNKNOWN}__UNKNOWN")
392
+ db_shard = Store.db_shard_for_target('UNKNOWN', scope: @scope)
393
+ Topic.del("#{@scope}__COMMAND__{UNKNOWN}__UNKNOWN", db_shard: db_shard)
394
+ Topic.del("#{@scope}__TELEMETRY__{UNKNOWN}__UNKNOWN", db_shard: db_shard)
394
395
  Topic.del("#{@scope}__openc3_targets")
395
396
  Topic.del("#{@scope}__CONFIG")
396
397
  end
@@ -66,17 +66,9 @@ module OpenC3
66
66
  keys = self.store.zrevrange("#{RUNNING_PRIMARY_KEY}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
67
67
  return [] if keys.empty?
68
68
  result = []
69
- if $openc3_redis_cluster
70
- # No pipelining for cluster mode
71
- # because it requires using the same shard for all keys
69
+ result = self.store.redis_pool.pipelined do
72
70
  keys.each do |key|
73
- result << self.store.hget("#{RUNNING_PRIMARY_KEY}__#{scope}", key)
74
- end
75
- else
76
- result = self.store.redis_pool.pipelined do
77
- keys.each do |key|
78
- self.store.hget("#{RUNNING_PRIMARY_KEY}__#{scope}", key)
79
- end
71
+ self.store.hget("#{RUNNING_PRIMARY_KEY}__#{scope}", key)
80
72
  end
81
73
  end
82
74
  result = result.map do |r|
@@ -91,17 +83,9 @@ module OpenC3
91
83
  keys = self.store.zrevrange("#{COMPLETED_PRIMARY_KEY}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
92
84
  return [] if keys.empty?
93
85
  result = []
94
- if $openc3_redis_cluster
95
- # No pipelining for cluster mode
96
- # because it requires using the same shard for all keys
86
+ result = self.store.redis_pool.pipelined do
97
87
  keys.each do |key|
98
- result << self.store.hget("#{COMPLETED_PRIMARY_KEY}__#{scope}", key)
99
- end
100
- else
101
- result = self.store.redis_pool.pipelined do
102
- keys.each do |key|
103
- self.store.hget("#{COMPLETED_PRIMARY_KEY}__#{scope}", key)
104
- end
88
+ self.store.hget("#{COMPLETED_PRIMARY_KEY}__#{scope}", key)
105
89
  end
106
90
  end
107
91
  result = result.map do |r|