openc3 7.1.1 → 7.2.1

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +11 -23
  3. data/data/config/command_modifiers.yaml +2 -2
  4. data/data/config/interface_modifiers.yaml +15 -0
  5. data/data/config/item_modifiers.yaml +10 -3
  6. data/data/config/target.yaml +17 -0
  7. data/data/config/widgets.yaml +28 -1
  8. data/lib/openc3/accessors/binary_accessor.rb +4 -0
  9. data/lib/openc3/api/cmd_api.rb +5 -1
  10. data/lib/openc3/api/limits_api.rb +72 -0
  11. data/lib/openc3/api/tlm_api.rb +6 -0
  12. data/lib/openc3/microservices/decom_common.rb +15 -4
  13. data/lib/openc3/microservices/decom_microservice.rb +6 -1
  14. data/lib/openc3/microservices/interface_microservice.rb +25 -11
  15. data/lib/openc3/microservices/microservice.rb +20 -5
  16. data/lib/openc3/microservices/queue_microservice.rb +4 -2
  17. data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
  18. data/lib/openc3/models/cvt_model.rb +2 -2
  19. data/lib/openc3/models/db_sharded_model.rb +2 -2
  20. data/lib/openc3/models/interface_model.rb +16 -10
  21. data/lib/openc3/models/interface_status_model.rb +2 -2
  22. data/lib/openc3/models/metric_model.rb +2 -2
  23. data/lib/openc3/models/microservice_status_model.rb +2 -2
  24. data/lib/openc3/models/model.rb +2 -6
  25. data/lib/openc3/models/plugin_model.rb +15 -14
  26. data/lib/openc3/models/python_package_model.rb +3 -1
  27. data/lib/openc3/models/reingest_job_model.rb +1 -1
  28. data/lib/openc3/models/script_status_model.rb +73 -38
  29. data/lib/openc3/models/target_model.rb +13 -0
  30. data/lib/openc3/operators/operator.rb +49 -14
  31. data/lib/openc3/packets/commands.rb +3 -3
  32. data/lib/openc3/packets/limits.rb +23 -0
  33. data/lib/openc3/packets/packet.rb +5 -3
  34. data/lib/openc3/packets/packet_config.rb +22 -5
  35. data/lib/openc3/packets/structure.rb +14 -10
  36. data/lib/openc3/script/limits.rb +1 -1
  37. data/lib/openc3/script/suite.rb +1 -1
  38. data/lib/openc3/script/suite_runner.rb +43 -9
  39. data/lib/openc3/script/web_socket_api.rb +6 -2
  40. data/lib/openc3/system/target.rb +4 -1
  41. data/lib/openc3/tools/table_manager/table_config.rb +4 -1
  42. data/lib/openc3/top_level.rb +1 -1
  43. data/lib/openc3/topics/limits_event_topic.rb +62 -3
  44. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
  45. data/lib/openc3/utilities/cli_generator.rb +432 -404
  46. data/lib/openc3/utilities/env_helper.rb +13 -1
  47. data/lib/openc3/utilities/pypi_url.rb +39 -0
  48. data/lib/openc3/utilities/questdb_client.rb +51 -4
  49. data/lib/openc3/utilities/reingest_job.rb +1 -1
  50. data/lib/openc3/utilities/running_script.rb +58 -15
  51. data/lib/openc3/utilities/script.rb +14 -3
  52. data/lib/openc3/utilities/simulated_target.rb +4 -2
  53. data/lib/openc3/utilities/store_autoload.rb +39 -1
  54. data/lib/openc3/utilities/store_queued.rb +5 -1
  55. data/lib/openc3/version.rb +5 -5
  56. data/lib/openc3.rb +6 -1
  57. data/templates/command_validator/command_validator.py +8 -10
  58. data/templates/command_validator/command_validator.rb +6 -9
  59. data/templates/plugin/LICENSE.md +16 -4
  60. data/templates/tool_angular/package.json +2 -2
  61. data/templates/tool_react/package.json +1 -1
  62. data/templates/tool_svelte/package.json +1 -1
  63. data/templates/tool_vue/package.json +3 -3
  64. data/templates/widget/package.json +2 -2
  65. metadata +3 -1
@@ -20,7 +20,7 @@ module OpenC3
20
20
  private
21
21
 
22
22
  # Define all the modification methods such that we can disconnect them
23
- %i(enable_limits disable_limits set_limits enable_limits_group disable_limits_group set_limits_set).each do |method_name|
23
+ %i(enable_limits disable_limits set_limits set_state_color enable_limits_group disable_limits_group set_limits_set delete_limits_set).each do |method_name|
24
24
  define_method(method_name) do |*args, **kw_args, &block|
25
25
  kw_args[:scope] = $openc3_scope unless kw_args[:scope]
26
26
  if $disconnect
@@ -287,7 +287,7 @@ module OpenC3
287
287
  # Find all the script methods
288
288
  methods = []
289
289
  self.instance_methods.each do |method_name|
290
- if /^test|^script|op_/.match?(method_name.to_s)
290
+ if /^test_|^script_|^op_/.match?(method_name.to_s)
291
291
  methods << method_name.to_s
292
292
  end
293
293
  end
@@ -26,6 +26,11 @@ module OpenC3
26
26
  end
27
27
 
28
28
  class SuiteRunner
29
+ # Default suite_runner options shared by all callers (e.g. openc3cli) so
30
+ # the values stay consistent in one place.
31
+ DEFAULT_METHOD = 'start'
32
+ DEFAULT_OPTIONS = ['continueAfterError'].freeze
33
+
29
34
  @@suites = []
30
35
  @@settings = {}
31
36
  @@suite_results = nil
@@ -42,17 +47,46 @@ module OpenC3
42
47
  @@suite_results
43
48
  end
44
49
 
50
+ # Validate the suite_runner option combinations that don't require the
51
+ # built suites. Shared by all callers (the running script via execute and
52
+ # openc3cli when constructing options) so the contract lives in one place.
53
+ # Suite/Group existence is checked in execute since it needs @@suites.
54
+ def self.validate_options(group: nil, script: nil)
55
+ raise ArgumentError, "Script #{script} requires a Group" if script and not group
56
+ end
57
+
58
+ # Build the canonical, validated suite_runner hash from raw values,
59
+ # applying defaults. Shared by openc3cli (constructing from CLI flags) and
60
+ # the running script (normalizing a received hash before dispatch) so the
61
+ # hash shape, defaults, and validation live in one place.
62
+ def self.build_options(suite:, group: nil, script: nil, method: nil, options: nil)
63
+ validate_options(group: group, script: script)
64
+ suite_runner = { 'suite' => suite }
65
+ if group
66
+ suite_runner['group'] = group
67
+ suite_runner['script'] = script if script
68
+ end
69
+ # A script always runs via start, matching the GUI
70
+ suite_runner['method'] = script ? DEFAULT_METHOD : (method || DEFAULT_METHOD)
71
+ suite_runner['options'] = options || DEFAULT_OPTIONS.dup
72
+ suite_runner
73
+ end
74
+
45
75
  def self.execute(result_string, suite_class, group_class = nil, script = nil)
46
76
  @@suite_results = SuiteResults.new
47
- @@suites.each do |suite|
48
- if suite.class == suite_class
49
- @@suite_results.start(result_string, suite_class, group_class, script, @@settings)
50
- loop do
51
- yield(suite)
52
- break if not @@settings['Loop'] or (ScriptStatus.instance.fail_count > 0 and @@settings['Break Loop On Error'])
53
- end
54
- break
55
- end
77
+ # Surface invalid suite / group / option combinations rather than
78
+ # silently running nothing or failing with an opaque nil error. An
79
+ # invalid script method raises later in Group#run_method.
80
+ validate_options(group: group_class, script: script)
81
+ suite = @@suites.find { |s| s.class == suite_class }
82
+ raise ArgumentError, "Suite #{suite_class} not found" unless suite
83
+ if group_class and not suite.scripts.key?(group_class)
84
+ raise ArgumentError, "Group #{group_class} not found in Suite #{suite_class}"
85
+ end
86
+ @@suite_results.start(result_string, suite_class, group_class, script, @@settings)
87
+ loop do
88
+ yield(suite)
89
+ break if not @@settings['Loop'] or (ScriptStatus.instance.fail_count > 0 and @@settings['Break Loop On Error'])
56
90
  end
57
91
  end
58
92
 
@@ -1,4 +1,4 @@
1
- # encoding: ascii-8bit
1
+ # encoding: utf-8
2
2
 
3
3
  # Copyright 2026 OpenC3, Inc.
4
4
  # All Rights Reserved.
@@ -91,6 +91,10 @@ module OpenC3
91
91
  # Will subscribe to the channel based on @identifier
92
92
  def subscribe
93
93
  unless @subscribed
94
+ # Token is part of the identifier so it surfaces as params[:token] in
95
+ # ApplicationCable::Channel#authenticate_subscription! — ActionCable
96
+ # ignores `data` on `subscribe` commands.
97
+ @identifier['token'] = @authentication.token(include_bearer: false)
94
98
  json_hash = {}
95
99
  json_hash['command'] = 'subscribe'
96
100
  json_hash['identifier'] = JSON.generate(@identifier, allow_nan: true)
@@ -128,7 +132,7 @@ module OpenC3
128
132
  # Connect to the websocket with authorization in query params
129
133
  def connect
130
134
  disconnect()
131
- final_url = @url + "?scope=#{@scope}&authorization=#{@authentication.get_otp(scope: @scope)}"
135
+ final_url = @url + "?scope=#{@scope}"
132
136
  @stream = WebSocketClientStream.new(final_url, @write_timeout, @read_timeout, @connect_timeout)
133
137
  @stream.headers = {
134
138
  'Sec-WebSocket-Protocol' => 'actioncable-v1-json, actioncable-unsupported',
@@ -102,7 +102,10 @@ module OpenC3
102
102
  def process_file(filename)
103
103
  Logger.instance.info "Processing target definition in file '#{filename}'"
104
104
  parser = ConfigParser.new("https://docs.openc3.com/docs/configuration/target")
105
- parser.parse_file(filename) do |keyword, parameters|
105
+ # run_erb is false: ERB is only rendered once, at plugin install time
106
+ # (TargetModel#deploy). At runtime target files are read already-rendered
107
+ # from the bucket, so they are treated as data, not re-executed as code.
108
+ parser.parse_file(filename, false, true, false) do |keyword, parameters|
106
109
  case keyword
107
110
  when 'LANGUAGE'
108
111
  usage = "#{keyword} <ruby | python>"
@@ -85,7 +85,10 @@ module OpenC3
85
85
  ConfigParser.new(
86
86
  'https://openc3.com/docs/tools/#table-manager-configuration-openc3--39',
87
87
  )
88
- parser.parse_file(filename) do |keyword, params|
88
+ # run_erb is false: ERB is only rendered once, at plugin install time
89
+ # (TargetModel#deploy). Table definitions are read already-rendered from the
90
+ # bucket here, so they are treated as data, not re-executed as code.
91
+ parser.parse_file(filename, false, true, false) do |keyword, params|
89
92
  if @building_generic_conversion
90
93
  case keyword
91
94
  # Complete a generic conversion
@@ -108,7 +108,7 @@ module OpenC3
108
108
  # This method is taken directly from the Rails source:
109
109
  # https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Filename.html#method-i-sanitized
110
110
  # NOTE: I removed the '/' character because we have to allow this in order to traverse the path
111
- sanitized = path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "").strip.tr("\u{202E}%$|:;\t\r\n\\", "-").gsub('..', '-')
111
+ sanitized = path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\u{FFFD}").strip.tr("\u{202E}%$|:;\t\r\n\\", "-").gsub('..', '-')
112
112
  if sanitized != path
113
113
  raise StorageError, "Invalid path: #{path}"
114
114
  end
@@ -39,9 +39,15 @@ module OpenC3
39
39
  case event[:type]
40
40
  when :LIMITS_CHANGE
41
41
  # The current_limits hash keeps only the current limits state of items
42
- # It is used by the API to determine the overall limits state
43
- field = "#{event[:target_name]}__#{event[:packet_name]}__#{event[:item_name]}"
44
- Store.hset("#{scope}__current_limits", field, event[:new_limits_state])
42
+ # It is used by the API to determine the overall limits state.
43
+ # When the event originates from a stored packet in a non-PROCESS mode
44
+ # (LOG or DISABLE), skip updating current_limits so that historical
45
+ # data does not affect the real-time overall limits state or the
46
+ # out_of_limits API response.
47
+ unless event[:suppress_stored]
48
+ field = "#{event[:target_name]}__#{event[:packet_name]}__#{event[:item_name]}"
49
+ Store.hset("#{scope}__current_limits", field, event[:new_limits_state])
50
+ end
45
51
 
46
52
  when :LIMITS_SETTINGS
47
53
  # Limits updated in limits_api.rb to avoid circular reference to TargetModel
@@ -79,6 +85,19 @@ module OpenC3
79
85
  limits_settings['enabled'] = event[:enabled]
80
86
  Store.hset("#{scope}__current_limits_settings", field, JSON.generate(limits_settings, allow_nan: true))
81
87
 
88
+ when :LIMITS_STATE_COLOR
89
+ # Persist the state color so it survives a decom microservice restart (applied by sync_system)
90
+ field = "#{event[:target_name]}__#{event[:packet_name]}__#{event[:item_name]}"
91
+ limits_settings = Store.hget("#{scope}__current_limits_settings", field)
92
+ if limits_settings
93
+ limits_settings = JSON.parse(limits_settings, allow_nan: true, create_additions: true)
94
+ else
95
+ limits_settings = {}
96
+ end
97
+ limits_settings['state_colors'] ||= {}
98
+ limits_settings['state_colors'][event[:state_name].to_s] = event[:color].to_s
99
+ Store.hset("#{scope}__current_limits_settings", field, JSON.generate(limits_settings, allow_nan: true))
100
+
82
101
  when :LIMITS_SET
83
102
  sets = sets(scope: scope)
84
103
  raise "Set '#{event[:set]}' does not exist!" unless sets.key?(event[:set])
@@ -173,6 +192,28 @@ module OpenC3
173
192
  end
174
193
  end
175
194
 
195
+ # Removes a limits set from the limits_sets hash and from the
196
+ # current_limits_settings of every item. Note that the set is not removed
197
+ # from the TargetModel packet definitions; that is cleaned up on the next
198
+ # plugin install. Running microservices will continue to hold the set in
199
+ # memory until they restart and resync from current_limits_settings.
200
+ def self.delete_set(set_name, scope:)
201
+ set_name = set_name.to_s
202
+ limits_settings = Store.hgetall("#{scope}__current_limits_settings")
203
+ # Collect all changed items and write them back in a single hmset to
204
+ # avoid a Redis round trip per item (this hash can be large)
205
+ updates = {}
206
+ limits_settings.each do |item, settings|
207
+ settings = JSON.parse(settings, allow_nan: true, create_additions: true)
208
+ if settings.key?(set_name)
209
+ settings.delete(set_name)
210
+ updates[item] = JSON.generate(settings, allow_nan: true)
211
+ end
212
+ end
213
+ Store.hmset("#{scope}__current_limits_settings", *updates.flatten) unless updates.empty?
214
+ Store.hdel("#{scope}__limits_sets", set_name)
215
+ end
216
+
176
217
  # Update the local System based on overall state
177
218
  def self.sync_system(scope:)
178
219
  all_limits_settings = Store.hgetall("#{scope}__current_limits_settings")
@@ -188,6 +229,12 @@ module OpenC3
188
229
  persistence = limits_settings['persistence_setting']
189
230
  limits_settings.each do |limits_set, settings|
190
231
  next unless Hash === settings
232
+ if limits_set == 'state_colors'
233
+ settings.each do |state_name, color|
234
+ System.limits.set_state_color(target_name, packet_name, item_name, state_name, color)
235
+ end
236
+ next
237
+ end
191
238
  System.limits.set(target_name, packet_name, item_name, settings['red_low'], settings['yellow_low'], settings['yellow_high'], settings['red_high'], settings['green_low'], settings['green_high'], limits_set.to_s.intern, persistence, enabled)
192
239
  end
193
240
  if not enabled.nil?
@@ -242,6 +289,18 @@ module OpenC3
242
289
  end
243
290
  end
244
291
 
292
+ when 'LIMITS_STATE_COLOR'
293
+ target_name = event['target_name']
294
+ packet_name = event['packet_name']
295
+ item_name = event['item_name']
296
+ target = telemetry[target_name]
297
+ if target
298
+ packet = target[packet_name]
299
+ if packet
300
+ System.limits.set_state_color(target_name, packet_name, item_name, event['state_name'], event['color'])
301
+ end
302
+ end
303
+
245
304
  when 'LIMITS_SET'
246
305
  System.limits_set = event['set']
247
306
  end
@@ -20,7 +20,7 @@ require 'openc3/utilities/open_telemetry'
20
20
 
21
21
  module OpenC3
22
22
  class TelemetryDecomTopic < Topic
23
- def self.write_packet(packet, id: nil, scope:)
23
+ def self.write_packet(packet, id: nil, include_limits_states: true, scope:)
24
24
  OpenC3.in_span("write_packet") do
25
25
  # Need to build a JSON hash of the decommutated data
26
26
  # Support "downward typing"
@@ -29,7 +29,7 @@ module OpenC3
29
29
  # If nothing - item does not exist - nil
30
30
  # __ as separators ITEM1, ITEM1__C, ITEM1__F
31
31
 
32
- json_hash = CvtModel.build_json_from_packet(packet)
32
+ json_hash = CvtModel.build_json_from_packet(packet, include_limits_states: include_limits_states)
33
33
  # Convert to JSON-safe types once and reuse for both topic write and CVT set
34
34
  json_safe_hash = json_hash.as_json
35
35
  json_data = JSON.generate(json_safe_hash, allow_nan: true)