openc3 7.2.0 → 7.3.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +108 -29
  3. data/bin/pipinstall +5 -4
  4. data/bin/pipuninstall +5 -1
  5. data/bin/uvinstall +149 -0
  6. data/data/config/interface_modifiers.yaml +15 -0
  7. data/data/config/item_modifiers.yaml +2 -1
  8. data/data/config/microservice.yaml +5 -1
  9. data/data/config/screen.yaml +2 -1
  10. data/data/config/target.yaml +38 -0
  11. data/data/config/tool.yaml +3 -1
  12. data/data/config/widgets.yaml +43 -3
  13. data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
  14. data/data/xtce_schemas/xml.xsd +116 -0
  15. data/ext/openc3/ext/telemetry/telemetry.c +1 -1
  16. data/lib/openc3/accessors/binary_accessor.rb +4 -0
  17. data/lib/openc3/accessors/template_accessor.rb +2 -2
  18. data/lib/openc3/api/cmd_api.rb +5 -1
  19. data/lib/openc3/api/interface_api.rb +2 -2
  20. data/lib/openc3/api/limits_api.rb +89 -3
  21. data/lib/openc3/api/router_api.rb +3 -3
  22. data/lib/openc3/api/tlm_api.rb +1 -1
  23. data/lib/openc3/interfaces/http_client_interface.rb +1 -1
  24. data/lib/openc3/io/json_drb.rb +24 -8
  25. data/lib/openc3/io/json_rpc.rb +0 -2
  26. data/lib/openc3/logs/log_writer.rb +43 -7
  27. data/lib/openc3/logs/packet_log_writer.rb +1 -2
  28. data/lib/openc3/microservices/decom_common.rb +15 -4
  29. data/lib/openc3/microservices/decom_microservice.rb +6 -1
  30. data/lib/openc3/microservices/interface_microservice.rb +26 -12
  31. data/lib/openc3/microservices/queue_microservice.rb +4 -2
  32. data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
  33. data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
  34. data/lib/openc3/models/cvt_model.rb +7 -7
  35. data/lib/openc3/models/db_sharded_model.rb +2 -2
  36. data/lib/openc3/models/gem_model.rb +1 -1
  37. data/lib/openc3/models/interface_model.rb +17 -11
  38. data/lib/openc3/models/interface_status_model.rb +2 -2
  39. data/lib/openc3/models/metric_model.rb +2 -2
  40. data/lib/openc3/models/microservice_model.rb +21 -0
  41. data/lib/openc3/models/microservice_status_model.rb +2 -2
  42. data/lib/openc3/models/model.rb +2 -6
  43. data/lib/openc3/models/plugin_model.rb +204 -49
  44. data/lib/openc3/models/python_package_model.rb +210 -12
  45. data/lib/openc3/models/reingest_job_model.rb +1 -1
  46. data/lib/openc3/models/scope_model.rb +34 -30
  47. data/lib/openc3/models/script_status_model.rb +73 -38
  48. data/lib/openc3/models/target_model.rb +193 -6
  49. data/lib/openc3/models/tool_model.rb +33 -20
  50. data/lib/openc3/operators/microservice_operator.rb +33 -4
  51. data/lib/openc3/operators/operator.rb +15 -5
  52. data/lib/openc3/packets/commands.rb +7 -7
  53. data/lib/openc3/packets/limits.rb +33 -2
  54. data/lib/openc3/packets/packet.rb +6 -4
  55. data/lib/openc3/packets/packet_config.rb +5 -1
  56. data/lib/openc3/packets/parsers/state_parser.rb +1 -1
  57. data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
  58. data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
  59. data/lib/openc3/packets/structure.rb +16 -12
  60. data/lib/openc3/packets/telemetry.rb +3 -3
  61. data/lib/openc3/script/commands.rb +1 -1
  62. data/lib/openc3/script/extract.rb +20 -25
  63. data/lib/openc3/script/limits.rb +1 -1
  64. data/lib/openc3/script/suite_runner.rb +43 -9
  65. data/lib/openc3/script/web_socket_api.rb +45 -5
  66. data/lib/openc3/system/target.rb +4 -1
  67. data/lib/openc3/tools/table_manager/table_config.rb +4 -1
  68. data/lib/openc3/top_level.rb +1 -1
  69. data/lib/openc3/topics/command_decom_topic.rb +16 -1
  70. data/lib/openc3/topics/limits_event_topic.rb +63 -4
  71. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
  72. data/lib/openc3/utilities/aws_bucket.rb +10 -2
  73. data/lib/openc3/utilities/cli_generator.rb +5 -1
  74. data/lib/openc3/utilities/csv.rb +5 -5
  75. data/lib/openc3/utilities/env_helper.rb +13 -1
  76. data/lib/openc3/utilities/local_mode.rb +4 -1
  77. data/lib/openc3/utilities/pypi_url.rb +39 -0
  78. data/lib/openc3/utilities/reingest_job.rb +1 -1
  79. data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
  80. data/lib/openc3/utilities/running_script.rb +160 -100
  81. data/lib/openc3/utilities/script.rb +60 -13
  82. data/lib/openc3/utilities/store_autoload.rb +39 -1
  83. data/lib/openc3/utilities/store_queued.rb +5 -1
  84. data/lib/openc3/version.rb +5 -5
  85. data/lib/openc3.rb +6 -1
  86. data/templates/plugin/plugin.gemspec +1 -1
  87. data/templates/tool_angular/package.json +2 -2
  88. data/templates/tool_react/package.json +1 -1
  89. data/templates/tool_svelte/package.json +1 -1
  90. data/templates/tool_vue/package.json +3 -3
  91. data/templates/widget/package.json +2 -2
  92. metadata +7 -3
  93. data/templates/tool_vue/.nycrc +0 -3
  94. data/templates/widget/.nycrc +0 -3
@@ -24,15 +24,46 @@ INVALID_CHARS = '[]./'
24
24
  REPLACEMENT_CHAR = '_'
25
25
  ALIAS_NAMESPACE = 'COSMOS'
26
26
 
27
- COMBINED_NAME = "COMBINED"
28
- MAX_64_BIT_INT = 18446744073709551614
27
+ # IntegerRangeType declares minInclusive / maxInclusive as xs:long, so an integer range
28
+ # outside these bounds (a full 64 bit UINT, for example) cannot be expressed as a
29
+ # ValidRange. FloatRangeType uses xs:double and needs no such clamp.
30
+ XS_LONG_MIN = -9223372036854775808
31
+ XS_LONG_MAX = 9223372036854775807
29
32
  COSMOS_NATIVE_DERIVED_ITEMS = ['PACKET_TIMESECONDS', 'PACKET_TIMEFORMATTED', 'RECEIVED_TIMESECONDS', 'RECEIVED_TIMEFORMATTED', 'RECEIVED_COUNT']
30
- PACKET_TIME_STRING = "PACKET_TIME"
31
33
 
32
34
  module OpenC3
33
35
  class XtceConverter
34
36
  attr_accessor :current_target_name
35
37
 
38
+ # The namespace COSMOS exports. Files declaring an older namespace still import.
39
+ XTCE_1_2_NAMESPACE = 'http://www.omg.org/spec/XTCE/20180204'
40
+ # The OMG schemas are vendored here and validation reads them from disk, so it works
41
+ # air gapped. The omg.org URL the files declare in schemaLocation is never fetched.
42
+ SCHEMA_DIR = File.join(PATH, 'data', 'xtce_schemas')
43
+ SCHEMA_FILE = File.join(SCHEMA_DIR, 'SpaceSystem_20180204.xsd')
44
+
45
+ # The XTCE namespace an existing .xtce file declares on its root element
46
+ #
47
+ # @param filename [String] Path to a .xtce file
48
+ # @return [String, nil] The declared namespace, or nil if the file has none
49
+ def self.xtce_namespace(filename)
50
+ Nokogiri::XML(File.read(filename)).root&.namespace&.href
51
+ end
52
+
53
+ # Validate an .xtce file against the vendored OMG XTCE 1.2 schema
54
+ #
55
+ # Only meaningful for files declaring XTCE_1_2_NAMESPACE. XTCE 1.0 / 1.1 files
56
+ # import fine but report errors against this schema, so check xtce_namespace first.
57
+ #
58
+ # @param filename [String] Path to a .xtce file
59
+ # @return [Array<String>] Validation error messages, empty when the file is valid
60
+ def self.schema_errors(filename)
61
+ # File.open, not File.read: the schema imports xml.xsd relative to itself, which
62
+ # Nokogiri can only resolve from the IO's path
63
+ @schema ||= File.open(SCHEMA_FILE) { |f| Nokogiri::XML::Schema(f) }
64
+ @schema.validate(Nokogiri::XML(File.read(filename))).map(&:message)
65
+ end
66
+
36
67
  # Output a previously parsed definition file into the XTCE format
37
68
  #
38
69
  # @param commands [Hash<String=>Packet>] Hash of all the command packets
@@ -206,19 +237,24 @@ module OpenC3
206
237
  # Gather and make unique all the packet items
207
238
  return unless telemetry[target_name]
208
239
 
209
- unique_items = telemetry[target_name] ? get_unique(telemetry[target_name]) : {}
240
+ unique_items = get_unique(telemetry[target_name])
241
+ has_packet_time = unique_items.include?(@packet_time_string)
210
242
 
211
243
  xml['xtce'].TelemetryMetaData do
212
- xml['xtce'].ParameterTypeSet do
213
- unique_items.each do |item_name, item|
214
- to_xtce_type(item, 'Parameter', xml)
244
+ # The schema requires at least one child in each set, so emit neither when the
245
+ # target's packets hold nothing but DERIVED items. DERIVED items produce a TODO
246
+ # comment at most, never a ParameterType, so they don't count here.
247
+ if unique_items.any? { |_item_name, item| item.data_type != :DERIVED }
248
+ xml['xtce'].ParameterTypeSet do
249
+ unique_items.each do |item_name, item|
250
+ to_xtce_type(item, 'Parameter', xml)
251
+ end
215
252
  end
216
- end
217
- has_packet_time = unique_items.include?(@packet_time_string)
218
253
 
219
- xml['xtce'].ParameterSet do
220
- unique_items.each do |item_name, item|
221
- to_xtce_item(item, 'Parameter', xml, has_packet_time: has_packet_time)
254
+ xml['xtce'].ParameterSet do
255
+ unique_items.each do |item_name, item|
256
+ to_xtce_item(item, 'Parameter', xml, has_packet_time: has_packet_time)
257
+ end
222
258
  end
223
259
  end
224
260
 
@@ -228,6 +264,20 @@ module OpenC3
228
264
  # Replaces invalid characters if any exist
229
265
  attrs = { :name => packet_name.tr(INVALID_CHARS, REPLACEMENT_CHAR) }
230
266
  attrs['shortDescription'] = packet.description if packet.description
267
+ container_name = packet_name.tr(INVALID_CHARS, REPLACEMENT_CHAR)
268
+ has_id_items = (packet.id_items && packet.id_items.length > 0)
269
+
270
+ # RestrictionCriteria only exists on a BaseContainer, so a packet with ID
271
+ # items needs something to inherit from: an abstract container holding the
272
+ # entries, which the concrete container then restricts. Pointing the
273
+ # BaseContainer at its own container instead would make it inherit from
274
+ # itself, which is a cycle for any consumer that resolves inheritance.
275
+ if has_id_items
276
+ xml['xtce'].SequenceContainer(:name => "#{container_name}_Base", :abstract => "true") do
277
+ process_entry_list(xml, packet, :TELEMETRY)
278
+ end
279
+ end
280
+
231
281
  xml['xtce'].SequenceContainer(attrs) do
232
282
  # Adds an alias if any invalid characters exist
233
283
  if packet_name.count(INVALID_CHARS) > 0
@@ -240,9 +290,10 @@ module OpenC3
240
290
  xml['xtce'].AncillaryData("true", :name => "ALLOW_SHORT")
241
291
  end
242
292
  end
243
- process_entry_list(xml, packet, :TELEMETRY)
244
- xml['xtce'].BaseContainer(:containerRef => (packet_name.tr(INVALID_CHARS, REPLACEMENT_CHAR))) do
245
- if packet.id_items && packet.id_items.length > 0
293
+ if has_id_items
294
+ # The entries come from the base container. EntryList is required.
295
+ xml['xtce'].EntryList
296
+ xml['xtce'].BaseContainer(:containerRef => "#{container_name}_Base") do
246
297
  xml['xtce'].RestrictionCriteria do
247
298
  xml['xtce'].ComparisonList do
248
299
  packet.id_items.each do |item|
@@ -251,6 +302,8 @@ module OpenC3
251
302
  end
252
303
  end
253
304
  end
305
+ else
306
+ process_entry_list(xml, packet, :TELEMETRY)
254
307
  end
255
308
  end # SequenceContainer
256
309
  end # telemetry.each
@@ -279,8 +332,6 @@ module OpenC3
279
332
  end
280
333
  end
281
334
  end
282
- #unique_command_args_without_ids = get_unique_without_ids(commands[target_name])
283
- #if unique_command_args_without_ids.size > 0
284
335
  xml['xtce'].ArgumentTypeSet do
285
336
  commands[target_name].each do |packet_name, packet|
286
337
  packet.items.each do |arg_name, arg|
@@ -292,36 +343,64 @@ module OpenC3
292
343
  end
293
344
  xml['xtce'].MetaCommandSet do
294
345
  commands[target_name].each do |packet_name, packet|
295
- attrs = { :name => packet_name.tr(INVALID_CHARS, REPLACEMENT_CHAR) }
346
+ command_name = packet_name.tr(INVALID_CHARS, REPLACEMENT_CHAR)
347
+ attrs = { :name => command_name }
296
348
  attrs['shortDescription'] = packet.description if packet.description
349
+ argument_list_sorted_items = get_sorted_non_id_items(packet.sorted_items)
350
+ has_id_items = (packet.id_items && packet.id_items.length > 0)
351
+
352
+ # Like telemetry above, the ID comparisons live in a RestrictionCriteria,
353
+ # which only exists on a BaseContainer, so emit an abstract MetaCommand
354
+ # holding the arguments and entries for the concrete one to inherit and
355
+ # restrict. Referencing its own CommandContainer would be a cycle.
356
+ if has_id_items
357
+ xml['xtce'].MetaCommand(:name => "#{command_name}_Base", :abstract => "true") do
358
+ if argument_list_sorted_items.size > 0
359
+ xml['xtce'].ArgumentList do
360
+ argument_list_sorted_items.each do |item|
361
+ to_xtce_item(item, 'Argument', xml, prefix: command_name + "_")
362
+ end
363
+ end # ArgumentList
364
+ end
365
+ xml['xtce'].CommandContainer(:name => "#{command_name}_CommandsBase") do
366
+ process_entry_list(xml, packet, :COMMAND, unique_tlm_params)
367
+ end # Command Container
368
+ end # MetaCommand
369
+ end
370
+
297
371
  xml['xtce'].MetaCommand(attrs) do
298
372
  if packet_name.count(INVALID_CHARS) > 0
299
373
  xml['xtce'].AliasSet do
300
374
  xml['xtce'].Alias(:nameSpace => ALIAS_NAMESPACE, :alias => packet_name)
301
375
  end # AliasSet
302
376
  end # If packet contains invalid chars
303
- argument_list_sorted_items = get_sorted_non_id_items(packet.sorted_items)
304
- if argument_list_sorted_items.size > 0
377
+ if has_id_items
378
+ xml['xtce'].BaseMetaCommand(:metaCommandRef => "#{command_name}_Base")
379
+ elsif argument_list_sorted_items.size > 0
305
380
  xml['xtce'].ArgumentList do
306
381
  argument_list_sorted_items.each do |item|
307
- to_xtce_item(item, 'Argument', xml, prefix: packet_name.tr(INVALID_CHARS, REPLACEMENT_CHAR) + "_")
382
+ to_xtce_item(item, 'Argument', xml, prefix: command_name + "_")
308
383
  end
309
384
  end # ArgumentList
310
385
  end # If Arguments List is greater than 0
311
- xml['xtce'].CommandContainer(:name => "#{packet_name.tr(INVALID_CHARS, REPLACEMENT_CHAR)}_Commands") do
312
- process_entry_list(xml, packet, :COMMAND, unique_tlm_params)
313
- #xml['xtce'].BaseContainer(:containerRef => "#{target_name}_#{packet_name}_CommandContainer")
314
- if packet.id_items && packet.id_items.length > 0
315
- packet.id_items.each do |item|
316
- xml['xtce'].BaseContainer(:containerRef => "#{packet_name.tr(INVALID_CHARS, REPLACEMENT_CHAR)}_Commands") do
317
- xml['xtce'].RestrictionCriteria do
318
- xml['xtce'].ComparisonList do
386
+ xml['xtce'].CommandContainer(:name => "#{command_name}_Commands") do
387
+ if has_id_items
388
+ # The entries come from the base container. EntryList is required.
389
+ xml['xtce'].EntryList
390
+ # A CommandContainer allows only one BaseContainer, so emit all ID
391
+ # comparisons in a single RestrictionCriteria/ComparisonList.
392
+ xml['xtce'].BaseContainer(:containerRef => "#{command_name}_CommandsBase") do
393
+ xml['xtce'].RestrictionCriteria do
394
+ xml['xtce'].ComparisonList do
395
+ packet.id_items.each do |item|
319
396
  item_prefix = unique_tlm_params.include?(item.name) ? "CMD_" : ""
320
397
  xml['xtce'].Comparison(:parameterRef => item_prefix + item.name.tr(INVALID_CHARS, REPLACEMENT_CHAR),:value => item.id_value)
321
398
  end
322
- end # Restriction Criteria
323
- end # Base Container
324
- end # for each packet ID item
399
+ end
400
+ end # Restriction Criteria
401
+ end # Base Container
402
+ else
403
+ process_entry_list(xml, packet, :COMMAND, unique_tlm_params)
325
404
  end # If id items
326
405
  end # Command Container
327
406
  end # MetaCommand
@@ -347,17 +426,34 @@ module OpenC3
347
426
  initial_value
348
427
  end
349
428
 
429
+ # Ending index of an array item's single dimension. XTCE indices are inclusive, so
430
+ # an array of N elements ends at N - 1. A non-positive bit size means a variable
431
+ # length array whose element count isn't known at export time, reported as a single
432
+ # element.
433
+ def array_ending_index(item)
434
+ return 0 if item.bit_size <= 0 || item.array_size <= 0
435
+
436
+ (item.array_size / item.bit_size) - 1
437
+ end
438
+
439
+ # Initial value for a String or Binary type. Pass :BLOCK / 'Binary' for binary
440
+ # items and :STRING / 'String' for string items.
350
441
  def get_string_or_block_initial_value(item, string_or_binary)
351
442
  initial_value = nil
352
443
  if item.default && !item.array_size
353
- unless item.default.is_printable?
444
+ if string_or_binary == :BLOCK || string_or_binary == 'Binary'
445
+ # Binary initialValue is xs:hexBinary: raw hex digits, no 0x prefix
446
+ initial_value = item.default.simple_formatted
447
+ elsif !item.default.is_printable? || item.default.upcase.start_with?('0X')
448
+ # XML can't carry the non printable bytes of a string default, so COSMOS
449
+ # writes them as a 0x prefixed hex escape. A printable default that starts
450
+ # with 0x has to be escaped the same way, otherwise the importer reads it
451
+ # back as binary and "0xABCD" round trips as the bytes \xAB\xCD.
354
452
  initial_value = '0x' + item.default.simple_formatted
355
453
  else
356
- if string_or_binary == :STRING
357
- initial_value = item.default.inspect
358
- else
359
- initial_value = item.default.inspect.unpack('H*').first
360
- end
454
+ # String initialValue is the value itself. Quoting it (inspect) would make
455
+ # the quotes part of the default for anything but our own importer.
456
+ initial_value = item.default
361
457
  end
362
458
  end
363
459
  initial_value
@@ -381,26 +477,6 @@ module OpenC3
381
477
  unique
382
478
  end
383
479
 
384
- def get_unique_without_ids(items)
385
- unique = {}
386
- items.each do |packet_name, packet|
387
- packet.sorted_items.each do |item|
388
- next if item.data_type == :DERIVED
389
- next if item.id_value
390
- unique[item.name.tr(INVALID_CHARS, REPLACEMENT_CHAR)] ||= []
391
- unique[item.name.tr(INVALID_CHARS, REPLACEMENT_CHAR)] << item
392
- end
393
- end
394
- unique.each do |item_name, unique_items|
395
- if unique_items.length <= 1
396
- unique[item_name] = unique_items[0]
397
- next
398
- end
399
- unique[item_name] = unique_items[0]
400
- end
401
- unique
402
- end
403
-
404
480
  def get_sorted_non_id_items(items)
405
481
  sorted_items = []
406
482
  items.each do |item|
@@ -445,10 +521,15 @@ module OpenC3
445
521
  packet.sorted_items.each do |item|
446
522
  next if item.data_type == :DERIVED
447
523
  temp_type = item.id_value ? "Parameter" : type
448
- prefix = (cmd_vs_tlm == :COMMAND && unique_tlm_params.include?(item.name)) ? "CMD_" : ""
524
+ # Only ID items become Parameters in CommandMetaData, where a name shared with
525
+ # a telemetry parameter is a real collision and gets a CMD_ prefix. Arguments
526
+ # are scoped to their MetaCommand and are declared unprefixed, so prefixing the
527
+ # reference here would point at an Argument that doesn't exist.
528
+ prefix = (temp_type == "Parameter" && cmd_vs_tlm == :COMMAND && unique_tlm_params.include?(item.name)) ? "CMD_" : ""
449
529
  if item.array_size
450
- # Requiring parameterRef for argument arrays appears to be a defect in the schema
451
- reference_symbol = temp_type == "Argument" ? :parameterRef : "#{temp_type.downcase}Ref".to_sym
530
+ # XTCE 1.2 defines dedicated argumentRef/parameterRef attributes for
531
+ # Array{Argument,Parameter}RefEntry, so derive the reference from the type.
532
+ reference_symbol = "#{temp_type.downcase}Ref".to_sym
452
533
  xml['xtce'].public_send("Array#{temp_type}RefEntry".intern, reference_symbol => prefix + item.name.tr(INVALID_CHARS, REPLACEMENT_CHAR)) do
453
534
  set_fixed_value(xml, item) if !packed
454
535
  xml['xtce'].DimensionList do
@@ -457,7 +538,7 @@ module OpenC3
457
538
  xml['xtce'].FixedValue(0)
458
539
  end
459
540
  xml['xtce'].EndingIndex do
460
- xml['xtce'].FixedValue((item.array_size / item.bit_size) - 1)
541
+ xml['xtce'].FixedValue(array_ending_index(item))
461
542
  end
462
543
  end
463
544
  end
@@ -520,7 +601,9 @@ module OpenC3
520
601
  end # StartingIndex
521
602
  xml['xtce'].EndingIndex do
522
603
  xml['xtce'].FixedValue do
523
- xml['xtce'].text 0 # OpenC3 Only supports one-dimensional arrays
604
+ # OpenC3 only supports one-dimensional arrays, which is the single
605
+ # Dimension above. The indices give that dimension's length.
606
+ xml['xtce'].text array_ending_index(item)
524
607
  end # FixedValue
525
608
  end # EndingIndex
526
609
  end # Dimension
@@ -611,16 +694,10 @@ module OpenC3
611
694
  xml['xtce'].IntegerDataEncoding(:sizeInBits => item.bit_size, :encoding => encoding)
612
695
  end
613
696
  end
697
+ # ValidRange comes from IntegerDataType and DefaultAlarm from
698
+ # IntegerParameterType, which extends it, so ValidRange must be emitted first.
699
+ to_xtce_valid_range(item, param_or_arg, xml)
614
700
  to_xtce_limits(item, xml)
615
- if item.range and item.range.last <= MAX_64_BIT_INT
616
- if param_or_arg == "Parameter"
617
- xml['xtce'].ValidRange(:minInclusive => item.range.first, :maxInclusive => item.range.last)
618
- else
619
- xml['xtce'].ValidRangeSet do
620
- xml['xtce'].ValidRange(:minInclusive => item.range.first, :maxInclusive => item.range.last)
621
- end
622
- end
623
- end
624
701
  end # Type
625
702
  end # if item.states
626
703
  end
@@ -648,15 +725,35 @@ module OpenC3
648
725
  xml['xtce'].FloatDataEncoding(:sizeInBits => item.bit_size, :encoding => 'IEEE754_1985')
649
726
  end
650
727
  end
728
+ # ValidRange comes from FloatDataType and DefaultAlarm from FloatParameterType,
729
+ # which extends it, so ValidRange must be emitted first.
730
+ to_xtce_valid_range(item, param_or_arg, xml)
651
731
  to_xtce_limits(item, xml)
652
- if item.range and item.range.last < MAX_64_BIT_INT
653
- if param_or_arg == "Parameter"
654
- xml['xtce'].ValidRange(:minInclusive => item.range.first, :maxInclusive => item.range.last)
655
- else
656
- xml['xtce'].ValidRangeSet do
657
- xml['xtce'].ValidRange(:minInclusive => item.range.first, :maxInclusive => item.range.last)
658
- end
659
- end
732
+ end
733
+ end
734
+
735
+ # Emit the item minimum / maximum as an XTCE ValidRange. Arguments wrap it in a
736
+ # ValidRangeSet; parameters use a bare ValidRange.
737
+ def to_xtce_valid_range(item, param_or_arg, xml)
738
+ return unless item.range
739
+
740
+ minimum = item.range.first
741
+ maximum = item.range.last
742
+ if item.data_type == :INT or item.data_type == :UINT
743
+ # IntegerRangeType is xs:long, so a wider range (a full 64 bit UINT, for
744
+ # example) can't be expressed and is dropped rather than emitted invalid.
745
+ return if minimum < XS_LONG_MIN or maximum > XS_LONG_MAX
746
+ else
747
+ # FloatRangeType is xs:double, which covers any finite float. Infinity and NaN
748
+ # have no valid xs:double lexical form here, so skip them.
749
+ return unless minimum.finite? and maximum.finite?
750
+ end
751
+
752
+ if param_or_arg == "Parameter"
753
+ xml['xtce'].ValidRange(:minInclusive => minimum, :maxInclusive => maximum)
754
+ else
755
+ xml['xtce'].ValidRangeSet do
756
+ xml['xtce'].ValidRange(:minInclusive => minimum, :maxInclusive => maximum)
660
757
  end
661
758
  end
662
759
  end
@@ -664,20 +761,11 @@ module OpenC3
664
761
  def to_xtce_string(item, param_or_arg, xml, string_or_binary, prefix: "")
665
762
  attrs = { :name => (prefix + item.name.tr(INVALID_CHARS, REPLACEMENT_CHAR) + '_Type') }
666
763
  attrs[:characterWidth] = 8 if string_or_binary == 'String'
667
- if item.default && !item.array_size
668
- unless item.default.is_printable?
669
- attrs[:initialValue] = '0x' + item.default.simple_formatted
670
- else
671
- if string_or_binary == 'String'
672
- attrs[:initialValue] = item.default.inspect
673
- else
674
- attrs[:initialValue] = item.default.inspect.unpack('H*').first
675
- end
676
- end
677
- end
764
+ initial_value = get_string_or_block_initial_value(item, string_or_binary)
765
+ attrs[:initialValue] = initial_value if initial_value
678
766
  attrs[:shortDescription] = item.description if item.description
679
767
  xml['xtce'].public_send(string_or_binary + param_or_arg + 'Type', attrs) do
680
- # Don't call to_xtce_endianness for Strings or Blocks
768
+ # Strings and Blocks don't get a byteOrder
681
769
  to_xtce_units(item, xml)
682
770
  if string_or_binary == 'String'
683
771
  xml['xtce'].StringDataEncoding(:encoding => 'UTF-8') do
@@ -764,7 +852,9 @@ module OpenC3
764
852
  xml['xtce'].Alias(:nameSpace => ALIAS_NAMESPACE, :alias => item.name)
765
853
  end
766
854
  end
767
- if has_packet_time
855
+ # The time item is the source of the association, so it doesn't get one -
856
+ # pointing it at itself says a parameter is its own timestamp.
857
+ if has_packet_time && item.name != @packet_time_string
768
858
  xml['xtce'].ParameterProperties do
769
859
  xml['xtce'].TimeAssociation(:parameterRef => @packet_time_string)
770
860
  end
@@ -783,16 +873,6 @@ module OpenC3
783
873
  end
784
874
  end
785
875
 
786
- def to_xtce_endianness(item, xml)
787
- if item.endianness == :LITTLE_ENDIAN and item.bit_size > 8
788
- xml['xtce'].ByteOrderList do
789
- (((item.bit_size - 1) / 8) + 1).times do |byte_significance|
790
- xml['xtce'].Byte(:byteSignificance => byte_significance)
791
- end
792
- end
793
- end
794
- end
795
-
796
876
  def to_xtce_conversion(item, xml)
797
877
  if item.read_conversion
798
878
  conversion = item.read_conversion
@@ -154,7 +154,10 @@ module OpenC3
154
154
  current_type
155
155
  end
156
156
 
157
- XTCE_IGNORED_ELEMENTS = ['text', 'AliasSet', 'Alias', 'Header']
157
+ XTCE_IGNORED_ELEMENTS = ['text', 'Alias', 'Header']
158
+
159
+ # Namespace COSMOS writes its own item names into when the XTCE name had to differ
160
+ COSMOS_ALIAS_NAMESPACE = 'COSMOS'
158
161
 
159
162
  def xtce_process_element(element)
160
163
  if XTCE_IGNORED_ELEMENTS.include?(element.name)
@@ -418,6 +421,30 @@ module OpenC3
418
421
  end
419
422
  @arguments[element["name"]] = @current_argument
420
423
 
424
+ when 'AliasSet'
425
+ # COSMOS records the original item name here whenever it had to write a different
426
+ # one: a name containing characters XTCE forbids, or the CMD_ prefix that keeps a
427
+ # command ID item from colliding with a telemetry parameter of the same name.
428
+ # Restoring it means a name survives an export / import round trip. References
429
+ # elsewhere in the document still use the XTCE name, which is what the parameter
430
+ # and argument lookups are keyed by, so nothing else has to change.
431
+ cosmos_alias = element.elements.find do |child|
432
+ child.name == 'Alias' && child['nameSpace'] == COSMOS_ALIAS_NAMESPACE
433
+ end
434
+ if cosmos_alias and cosmos_alias['alias']
435
+ case element.parent&.name
436
+ when 'Parameter'
437
+ @current_parameter&.cosmos_name = cosmos_alias['alias']
438
+ when 'Argument'
439
+ @current_argument&.cosmos_name = cosmos_alias['alias']
440
+ else
441
+ # Do Nothing - COSMOS also writes aliases on SequenceContainer, MetaCommand
442
+ # and SpaceSystem to carry the original packet and target names. Those are
443
+ # not restored: a packet is registered under its name as soon as it is
444
+ # created, so renaming it here would leave the registration disagreeing.
445
+ end
446
+ end
447
+
421
448
  when 'ParameterProperties'
422
449
  element.attributes.each do |att_name, att|
423
450
  @current_parameter[att.name] = att.value
@@ -452,8 +479,10 @@ module OpenC3
452
479
  # Handled in MetaCommand
453
480
 
454
481
  when 'Comparison'
455
- # Need to set ID value for item
456
- item = @current_packet.get_item(element['parameterRef'])
482
+ # Need to set ID value for item. The reference is the XTCE name, which is not the
483
+ # item's name when an alias restored the original.
484
+ parameter = @parameters[element['parameterRef']]
485
+ item = @current_packet.get_item(parameter ? cosmos_item_name(parameter) : element['parameterRef'])
457
486
  item.id_value = Integer(element['value'])
458
487
  if @current_cmd_or_tlm == PacketConfig::COMMAND
459
488
  item.default = item.id_value
@@ -476,8 +505,9 @@ module OpenC3
476
505
  xtce_handle_base_container('BaseContainer', element)
477
506
 
478
507
  when 'ArgumentAssignment'
479
- # Need to set ID value for item
480
- item = @current_packet.get_item(element['argumentName'])
508
+ # Need to set ID value for item, by the item's name rather than the XTCE name
509
+ argument = @arguments[element['argumentName']]
510
+ item = @current_packet.get_item(argument ? cosmos_item_name(argument) : element['argumentName'])
481
511
  value = element['argumentValue']
482
512
  if item.states && item.states[value.to_s.upcase]
483
513
  item.id_value = item.states[value.to_s.upcase]
@@ -496,9 +526,16 @@ module OpenC3
496
526
  return true # Recurse further
497
527
  end
498
528
 
529
+ # The COSMOS name of a parameter / argument: its alias when the XTCE name had to
530
+ # differ from it, otherwise the XTCE name itself
531
+ def cosmos_item_name(object)
532
+ object.cosmos_name || object.name
533
+ end
534
+
499
535
  def process_ref_entry(element)
500
536
  reference_location, bit_offset = xtce_handle_location_in_container_in_bits(element)
501
537
  object, type, data_type, array_type = get_object_types(element)
538
+ item_name = cosmos_item_name(object)
502
539
  bit_size = Integer(type.sizeInBits)
503
540
  if array_type
504
541
  array_bit_size = process_array_type(element, bit_size)
@@ -509,16 +546,18 @@ module OpenC3
509
546
  if bit_offset
510
547
  case reference_location
511
548
  when 'containerStart'
512
- item = @current_packet.define_item(object.name, bit_offset, bit_size, data_type, array_bit_size, type.endianness) # overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
549
+ item = @current_packet.define_item(item_name, bit_offset, bit_size, data_type, array_bit_size, type.endianness) # overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
513
550
  when 'containerEnd'
514
- item = @current_packet.define_item(object.name, -bit_offset, bit_size, data_type, array_bit_size, type.endianness) # overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
551
+ item = @current_packet.define_item(item_name, -bit_offset, bit_size, data_type, array_bit_size, type.endianness) # overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
515
552
  when 'previousEntry', nil
516
- item = @current_packet.define_item(object.name, @current_packet.length + bit_offset, bit_size, data_type, array_bit_size, type.endianness) # overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
553
+ # Packet#length is bytes while the location is bits, so the offset is measured
554
+ # from the end of what has been defined so far in bits
555
+ item = @current_packet.define_item(item_name, (@current_packet.length * 8) + bit_offset, bit_size, data_type, array_bit_size, type.endianness) # overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
517
556
  when 'nextEntry'
518
557
  raise 'nextEntry is not supported'
519
558
  end
520
559
  else
521
- item = @current_packet.append_item(object.name, bit_size, data_type, array_bit_size, type.endianness) # overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
560
+ item = @current_packet.append_item(item_name, bit_size, data_type, array_bit_size, type.endianness) # overflow = :ERROR, format_string = nil, read_conversion = nil, write_conversion = nil, id_value = nil)
522
561
  end
523
562
 
524
563
  item.description = type.shortDescription if type.shortDescription
@@ -534,15 +573,15 @@ module OpenC3
534
573
  if /Parameter/.match?(element.name)
535
574
  # Look up the parameter and parameter type
536
575
  parameter = @parameters[element['parameterRef']]
537
- raise "parameterRef #{element['parameterRef']} not found" unless parameter
576
+ raise "parameterRef #{element['parameterRef']} not found (parameter lookup)" unless parameter
538
577
 
539
578
  parameter_type = @parameter_types[parameter.parameterTypeRef]
540
- raise "parameterTypeRef #{parameter.parameterTypeRef} not found" unless parameter_type
579
+ raise "parameterTypeRef #{parameter.parameterTypeRef} not found (parameter type lookup)" unless parameter_type
541
580
 
542
581
  if element.name == 'ArrayParameterRefEntry'
543
582
  array_type = parameter_type
544
583
  parameter_type = @parameter_types[array_type.arrayTypeRef]
545
- raise "arrayTypeRef #{parameter.arrayTypeRef} not found" unless parameter_type
584
+ raise "arrayTypeRef #{parameter.arrayTypeRef} not found (array parameter type)" unless parameter_type
546
585
  end
547
586
  refName = 'parameterRef'
548
587
  object = parameter
@@ -550,24 +589,25 @@ module OpenC3
550
589
  else
551
590
  # Look up the argument and argument type
552
591
  if element.name == 'ArrayArgumentRefEntry'
553
- # Requiring parameterRef for argument arrays appears to be a defect in the schema
554
- argument = @arguments[element['parameterRef']]
555
- raise "parameterRef #{element['parameterRef']} not found" unless argument
592
+ # XTCE 1.2 requires argumentRef on ArrayArgumentRefEntry. Older XTCE
593
+ # (1.0/1.1) and files exported by prior COSMOS versions used parameterRef
594
+ # here (a defect in those schemas), so accept it as a fallback.
595
+ refName = element['argumentRef'] ? 'argumentRef' : 'parameterRef'
596
+ argument = @arguments[element[refName]]
597
+ raise "#{refName} #{element[refName]} not found (array argument lookup)" unless argument
556
598
 
557
599
  argument_type = @argument_types[argument.argumentTypeRef]
558
- raise "argumentTypeRef #{argument.argumentTypeRef} not found" unless argument_type
600
+ raise "argumentTypeRef #{argument.argumentTypeRef} not found (array argument type lookup)" unless argument_type
559
601
 
560
602
  array_type = argument_type
561
603
  argument_type = @argument_types[array_type.arrayTypeRef]
562
- raise "arrayTypeRef #{array_type.arrayTypeRef} not found" unless argument_type
563
-
564
- refName = 'parameterRef'
604
+ raise "arrayTypeRef #{array_type.arrayTypeRef} not found (array argument element type)" unless argument_type
565
605
  else
566
606
  argument = @arguments[element['argumentRef']]
567
- raise "argumentRef #{element['argumentRef']} not found" unless argument
607
+ raise "argumentRef #{element['argumentRef']} not found (argument lookup)" unless argument
568
608
 
569
609
  argument_type = @argument_types[argument.argumentTypeRef]
570
- raise "argumentTypeRef #{argument.argumentTypeRef} not found" unless argument_type
610
+ raise "argumentTypeRef #{argument.argumentTypeRef} not found (argument type lookup)" unless argument_type
571
611
 
572
612
  refName = 'argumentRef'
573
613
  end
@@ -692,10 +732,18 @@ module OpenC3
692
732
  item.default = []
693
733
  else
694
734
  if type.initialValue
695
- if type.initialValue.upcase.start_with?("0X")
735
+ if data_type == :BLOCK
736
+ # Binary initialValue is xs:hexBinary (raw hex, optional 0x prefix)
737
+ begin
738
+ item.default = type.initialValue.hex_to_byte_string
739
+ rescue ArgumentError, TypeError => e
740
+ raise "#{item.name} initialValue '#{type.initialValue}' is not valid hexBinary (#{e.message})"
741
+ end
742
+ elsif type.initialValue.upcase.start_with?("0X")
696
743
  item.default = type.initialValue.hex_to_byte_string
697
744
  else
698
- # Strip quotes from strings
745
+ # Strip the quotes COSMOS 7.2 and earlier wrote around string defaults.
746
+ # Current exports write the value unquoted.
699
747
  if type.initialValue[0] == '"' && type.initialValue[-1] == '"'
700
748
  item.default = type.initialValue[1..-2]
701
749
  else