openc3 6.10.2 → 6.10.4

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +15 -4
  3. data/data/config/item_modifiers.yaml +2 -2
  4. data/data/config/parameter_modifiers.yaml +2 -2
  5. data/data/config/plugins.yaml +38 -0
  6. data/data/config/screen.yaml +23 -0
  7. data/data/config/target.yaml +8 -3
  8. data/data/config/widgets.yaml +30 -0
  9. data/ext/openc3/ext/config_parser/config_parser.c +49 -37
  10. data/lib/openc3/accessors/accessor.rb +2 -2
  11. data/lib/openc3/accessors/json_accessor.rb +1 -1
  12. data/lib/openc3/api/tlm_api.rb +1 -6
  13. data/lib/openc3/config/config_parser.rb +45 -25
  14. data/lib/openc3/io/json_api_object.rb +38 -14
  15. data/lib/openc3/io/json_drb_object.rb +29 -11
  16. data/lib/openc3/io/json_rpc.rb +20 -9
  17. data/lib/openc3/microservices/interface_microservice.rb +8 -3
  18. data/lib/openc3/models/plugin_model.rb +40 -9
  19. data/lib/openc3/models/target_model.rb +2 -1
  20. data/lib/openc3/packets/packet.rb +5 -2
  21. data/lib/openc3/packets/packet_config.rb +4 -2
  22. data/lib/openc3/packets/parsers/packet_item_parser.rb +9 -0
  23. data/lib/openc3/packets/parsers/xtce_converter.rb +464 -100
  24. data/lib/openc3/script/web_socket_api.rb +1 -1
  25. data/lib/openc3/topics/command_decom_topic.rb +3 -2
  26. data/lib/openc3/utilities/cli_generator.rb +1 -1
  27. data/lib/openc3/version.rb +5 -5
  28. data/templates/plugin/plugin.gemspec +1 -1
  29. data/templates/tool_angular/package.json +2 -2
  30. data/templates/tool_react/package.json +1 -1
  31. data/templates/tool_svelte/package.json +1 -1
  32. data/templates/tool_vue/package.json +3 -3
  33. data/templates/widget/package.json +2 -2
  34. metadata +15 -1
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2025, OpenC3, Inc.
17
+ # All changes Copyright 2026, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -58,21 +58,32 @@ end
58
58
  class String
59
59
  NON_ASCII_PRINTABLE = /[^\x21-\x7e\s]/
60
60
  NON_UTF8_PRINTABLE = /[\x00-\x08\x0E-\x1F\x7F]/
61
- def as_json(_options = nil)
62
- # If string is ASCII-8BIT (binary) and has non-ASCII bytes (> 127), encode as binary
63
- # This handles data from hex_to_byte_string and other binary sources
64
- if self.encoding == Encoding::ASCII_8BIT && self.bytes.any? { |b| b > 127 }
65
- return self.to_json_raw_object
66
- end
61
+ # Matches characters outside the Latin range (U+0000-U+00FF) or C1 control characters (U+0080-U+009F)
62
+ # Latin range covers Basic Latin (U+0000-U+007F) and Latin-1 Supplement (U+00A0-U+00FF)
63
+ # This includes common characters like µ (U+00B5), ° (U+00B0), ñ (U+00F1), etc.
64
+ OUTSIDE_LATIN_RANGE = /[^\u0000-\u007F\u00A0-\u00FF]/
67
65
 
66
+ def as_json(_options = nil)
67
+ # Try to interpret the string as UTF-8
68
+ # This handles both:
69
+ # 1. Unicode text in ASCII-8BIT strings (e.g., "µA" for micro-Ampères from config files)
70
+ # 2. Binary data from hex_to_byte_string (e.g., \xDE\xAD\xBE\xEF) which will fail valid_encoding?
68
71
  as_utf8 = self.dup.force_encoding('UTF-8')
69
72
  if as_utf8.valid_encoding?
73
+ # Valid UTF-8 - check for non-printable control characters
70
74
  if as_utf8 =~ NON_UTF8_PRINTABLE
71
75
  return self.to_json_raw_object
72
- else
73
- return as_utf8
74
76
  end
77
+ # Check if all characters are in the expected Latin range (U+0000-U+00FF)
78
+ # This prevents binary data that happens to be valid UTF-8 from being treated as text
79
+ # For example, \xDE\xAD decodes to U+07AD (Thaana script) which should be treated as binary
80
+ # Also reject C1 control characters (U+0080-U+009F) which are non-printable
81
+ if as_utf8 =~ OUTSIDE_LATIN_RANGE
82
+ return self.to_json_raw_object
83
+ end
84
+ return as_utf8
75
85
  else
86
+ # Invalid UTF-8 means this is truly binary data, encode as raw object
76
87
  return self.to_json_raw_object
77
88
  end
78
89
  end #:nodoc:
@@ -89,6 +89,7 @@ module OpenC3
89
89
  InterfaceTopic.receive_commands(@interface, scope: @scope) do |topic, msg_id, msg_hash, _redis|
90
90
  OpenC3.with_context(msg_hash) do
91
91
  release_critical = false
92
+ critical_model = nil
92
93
  msgid_seconds_from_epoch = msg_id.split('-')[0].to_i / 1000.0
93
94
  delta = Time.now.to_f - msgid_seconds_from_epoch
94
95
  @metric.set(name: 'interface_topic_delta_seconds', value: delta, type: 'gauge', unit: 'seconds', help: 'Delta time between data written to stream and interface cmd start') if @metric
@@ -188,9 +189,9 @@ module OpenC3
188
189
  end
189
190
  if msg_hash.key?('release_critical')
190
191
  # Note: intentional fall through below this point
191
- model = CriticalCmdModel.get_model(name: msg_hash['release_critical'], scope: @scope)
192
- if model
193
- msg_hash = model.cmd_hash
192
+ critical_model = CriticalCmdModel.get_model(name: msg_hash['release_critical'], scope: @scope)
193
+ if critical_model
194
+ msg_hash = critical_model.cmd_hash
194
195
  release_critical = true
195
196
  else
196
197
  next "Critical command #{msg_hash['release_critical']} not found"
@@ -279,6 +280,10 @@ module OpenC3
279
280
  command.extra ||= {}
280
281
  command.extra['cmd_string'] = msg_hash['cmd_string']
281
282
  command.extra['username'] = msg_hash['username']
283
+ # Add approver info if this was a critical command that was approved
284
+ if critical_model
285
+ command.extra['approver'] = critical_model.approver
286
+ end
282
287
  hazardous, hazardous_description = System.commands.cmd_pkt_hazardous?(command)
283
288
 
284
289
  # Initial Are you sure? Hazardous check
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2026, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -125,11 +125,13 @@ module OpenC3
125
125
 
126
126
  # Phase 1 Gather Variables
127
127
  variables = {}
128
+ current_variable_name = nil
128
129
  parser.parse_file(plugin_txt_path,
129
130
  false,
130
131
  true,
131
132
  false) do |keyword, params|
132
- if keyword == 'VARIABLE'
133
+ case keyword
134
+ when 'VARIABLE'
133
135
  usage = "#{keyword} <Variable Name> <Default Value>"
134
136
  parser.verify_num_parameters(2, nil, usage)
135
137
  variable_name = params[0]
@@ -137,10 +139,33 @@ module OpenC3
137
139
  raise "VARIABLE name '#{variable_name}' is reserved"
138
140
  end
139
141
  value = params[1..-1].join(" ")
140
- variables[variable_name] = value
142
+ variables[variable_name] = { 'value' => value }
143
+ current_variable_name = variable_name
141
144
  if existing_variables && existing_variables.key?(variable_name)
142
- variables[variable_name] = existing_variables[variable_name]
145
+ existing = existing_variables[variable_name]
146
+ # Handle both old format (string) and new format (hash)
147
+ if existing.is_a?(Hash)
148
+ variables[variable_name]['value'] = existing['value']
149
+ else
150
+ variables[variable_name]['value'] = existing
151
+ end
152
+ end
153
+ when 'VARIABLE_DESCRIPTION'
154
+ usage = "#{keyword} <Description>"
155
+ parser.verify_num_parameters(1, 1, usage)
156
+ unless current_variable_name
157
+ raise "VARIABLE_DESCRIPTION must follow a VARIABLE definition"
143
158
  end
159
+ variables[current_variable_name]['description'] = params[0]
160
+ when 'VARIABLE_STATE'
161
+ usage = "#{keyword} <Display Text> <Value>"
162
+ parser.verify_num_parameters(2, 2, usage)
163
+ unless current_variable_name
164
+ raise "VARIABLE_STATE must follow a VARIABLE definition"
165
+ end
166
+ variables[current_variable_name]['options'] ||= []
167
+ option = { 'value' => params[1], 'text' => params[0] }
168
+ variables[current_variable_name]['options'] << option
144
169
  end
145
170
  end
146
171
 
@@ -285,20 +310,26 @@ module OpenC3
285
310
  plugin_txt_path = tf.path
286
311
  variables = plugin_hash['variables']
287
312
  variables ||= {}
288
- variables['scope'] = scope
313
+ # Extract simple key-value pairs for ERB substitution
314
+ # Variables can be either new format (hash with 'value' key) or old format (string)
315
+ erb_variables = {}
316
+ variables.each do |name, var|
317
+ erb_variables[name] = var.is_a?(Hash) ? var['value'] : var
318
+ end
319
+ erb_variables['scope'] = scope
289
320
  if File.exist?(plugin_txt_path)
290
321
  parser = OpenC3::ConfigParser.new("https://openc3.com")
291
322
 
292
323
  current_model = nil
293
- parser.parse_file(plugin_txt_path, false, true, true, variables) do |keyword, params|
324
+ parser.parse_file(plugin_txt_path, false, true, true, erb_variables) do |keyword, params|
294
325
  case keyword
295
- when 'VARIABLE', 'NEEDS_DEPENDENCIES'
326
+ when 'VARIABLE', 'VARIABLE_DESCRIPTION', 'VARIABLE_STATE', 'NEEDS_DEPENDENCIES'
296
327
  # Ignore during phase 2
297
328
  when 'TARGET', 'INTERFACE', 'ROUTER', 'MICROSERVICE', 'TOOL', 'WIDGET', 'SCRIPT_ENGINE'
298
329
  begin
299
330
  if current_model
300
331
  current_model.create unless validate_only
301
- current_model.deploy(gem_path, variables, validate_only: validate_only)
332
+ current_model.deploy(gem_path, erb_variables, validate_only: validate_only)
302
333
  end
303
334
  # If something goes wrong in create, or more likely in deploy,
304
335
  # we want to clear the current_model and try to instantiate the next
@@ -318,7 +349,7 @@ module OpenC3
318
349
  end
319
350
  if current_model
320
351
  current_model.create unless validate_only
321
- current_model.deploy(gem_path, variables, validate_only: validate_only)
352
+ current_model.deploy(gem_path, erb_variables, validate_only: validate_only)
322
353
  current_model = nil
323
354
  end
324
355
  end
@@ -638,7 +638,8 @@ module OpenC3
638
638
  system = System.new([@name], temp_dir)
639
639
  if variables["xtce_output"]
640
640
  puts "Converting target #{@name} to .xtce files in #{variables["xtce_output"]}/#{@name}"
641
- system.packet_config.to_xtce(variables["xtce_output"])
641
+ puts " Using mnemonic '#{variables['time_association_name']}' as the packet time item."
642
+ system.packet_config.to_xtce(variables["xtce_output"], variables['time_association_name'])
642
643
  end
643
644
  unless validate_only
644
645
  build_target_archive(temp_dir, target_folder)
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2025, OpenC3, Inc.
17
+ # All changes Copyright 2026, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -433,6 +433,9 @@ module OpenC3
433
433
  previous_item = nil
434
434
  warnings = []
435
435
  @sorted_items.each do |item|
436
+ # Skip items with a parent_item since those are accessor-based items within a structure
437
+ # (e.g., JSON, CBOR) that don't have meaningful bit positions - they share the parent's bit_offset
438
+ next if item.parent_item
436
439
  if expected_next_offset and (item.bit_offset < expected_next_offset) and !item.overlap
437
440
  msg = "Bit definition overlap at bit offset #{item.bit_offset} for packet #{@target_name} #{@packet_name} items #{item.name} and #{previous_item.name}"
438
441
  Logger.instance.warn(msg)
@@ -1524,7 +1527,7 @@ module OpenC3
1524
1527
  value = value.to_s
1525
1528
  end
1526
1529
  end
1527
- value << ' ' << item.units if value_type == :WITH_UNITS and item.units
1530
+ value = "#{value} #{item.units}" if value_type == :WITH_UNITS and item.units
1528
1531
  value
1529
1532
  end
1530
1533
 
@@ -180,6 +180,7 @@ module OpenC3
180
180
  @converted_type,
181
181
  @converted_bit_size) if keyword.include? "WRITE"
182
182
  @building_generic_conversion = false
183
+ parser.set_preserve_lines(false)
183
184
  # Add the current config.line to the conversion being built
184
185
  else
185
186
  @proc_text << parser.line << "\n"
@@ -331,8 +332,8 @@ module OpenC3
331
332
  end
332
333
  end # def to_config
333
334
 
334
- def to_xtce(output_dir)
335
- XtceConverter.convert(@commands, @telemetry, output_dir)
335
+ def to_xtce(output_dir, time_association_name)
336
+ XtceConverter.convert(@commands, @telemetry, output_dir, time_association_name)
336
337
  end
337
338
 
338
339
  # Add current packet into hash if it exists
@@ -712,6 +713,7 @@ module OpenC3
712
713
  parser.verify_num_parameters(0, 2, usage)
713
714
  @proc_text = ''
714
715
  @building_generic_conversion = true
716
+ parser.set_preserve_lines(true)
715
717
  @converted_type = nil
716
718
  @converted_bit_size = nil
717
719
  if params[0]
@@ -65,6 +65,15 @@ module OpenC3
65
65
  @parser.verify_num_parameters(max_options - 2, max_options, @usage)
66
66
  end
67
67
  @parser.verify_parameter_naming(1) # Item name is the 1st parameter
68
+
69
+ # ARRAY items cannot have brackets in their name because brackets are used
70
+ # for array indexing in the UI and would cause confusion
71
+ if @parser.keyword.include?('ARRAY')
72
+ item_name = @parser.parameters[0]
73
+ if item_name.include?('[') || item_name.include?(']')
74
+ raise @parser.error("ARRAY items cannot have brackets in their name: #{item_name}", @usage)
75
+ end
76
+ end
68
77
  end
69
78
 
70
79
  def create_packet_item(packet, cmd_or_tlm)