openc3 7.4.1 → 7.5.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/bin/pipinstall +18 -5
  3. data/bin/pipuninstall +1 -1
  4. data/bin/uvinstall +36 -8
  5. data/data/config/default_plugins.yaml +43 -0
  6. data/data/config/interface_modifiers.yaml +22 -7
  7. data/data/config/item_modifiers.yaml +14 -4
  8. data/data/config/plugins.yaml +1 -1
  9. data/data/config/widgets.yaml +1 -1
  10. data/lib/openc3/api/cmd_api.rb +10 -13
  11. data/lib/openc3/api/interface_api.rb +1 -2
  12. data/lib/openc3/api/limits_api.rb +149 -160
  13. data/lib/openc3/api/metrics_api.rb +26 -1
  14. data/lib/openc3/api/router_api.rb +1 -2
  15. data/lib/openc3/api/tlm_api.rb +225 -71
  16. data/lib/openc3/config/config_parser.rb +3 -1
  17. data/lib/openc3/core_ext/tempfile.rb +2 -0
  18. data/lib/openc3/interfaces/file_interface.rb +6 -1
  19. data/lib/openc3/interfaces/http_client_interface.rb +14 -6
  20. data/lib/openc3/interfaces/interface.rb +27 -3
  21. data/lib/openc3/interfaces/read_queue_interface.rb +331 -0
  22. data/lib/openc3/interfaces/stream_interface.rb +22 -3
  23. data/lib/openc3/interfaces/tcpip_server_interface.rb +13 -0
  24. data/lib/openc3/interfaces/udp_interface.rb +16 -2
  25. data/lib/openc3/interfaces.rb +1 -0
  26. data/lib/openc3/logs/log_writer.rb +14 -4
  27. data/lib/openc3/microservices/decom_microservice.rb +30 -5
  28. data/lib/openc3/microservices/interface_decom_common.rb +3 -1
  29. data/lib/openc3/microservices/interface_microservice.rb +146 -50
  30. data/lib/openc3/microservices/log_microservice.rb +1 -3
  31. data/lib/openc3/microservices/microservice.rb +25 -1
  32. data/lib/openc3/microservices/plugin_microservice.rb +32 -0
  33. data/lib/openc3/microservices/text_log_microservice.rb +1 -3
  34. data/lib/openc3/models/cvt_model.rb +7 -2
  35. data/lib/openc3/models/interface_model.rb +1 -1
  36. data/lib/openc3/models/interface_status_model.rb +6 -0
  37. data/lib/openc3/models/metric_model.rb +52 -4
  38. data/lib/openc3/models/microservice_model.rb +7 -3
  39. data/lib/openc3/models/plugin_model.rb +12 -12
  40. data/lib/openc3/models/python_package_model.rb +3 -13
  41. data/lib/openc3/models/target_model.rb +92 -15
  42. data/lib/openc3/operators/microservice_operator.rb +5 -0
  43. data/lib/openc3/operators/operator.rb +69 -0
  44. data/lib/openc3/packets/packet.rb +10 -4
  45. data/lib/openc3/packets/packet_config.rb +12 -1
  46. data/lib/openc3/packets/packet_item.rb +40 -22
  47. data/lib/openc3/packets/parsers/state_parser.rb +1 -1
  48. data/lib/openc3/packets/structure_item.rb +3 -2
  49. data/lib/openc3/script/api_shared.rb +62 -86
  50. data/lib/openc3/script/extract.rb +263 -20
  51. data/lib/openc3/script/plugins.rb +4 -11
  52. data/lib/openc3/script/script.rb +1 -1
  53. data/lib/openc3/script/storage.rb +81 -10
  54. data/lib/openc3/system/system.rb +9 -6
  55. data/lib/openc3/topics/command_decom_topic.rb +1 -1
  56. data/lib/openc3/topics/command_topic.rb +2 -1
  57. data/lib/openc3/topics/limits_event_topic.rb +90 -3
  58. data/lib/openc3/topics/router_topic.rb +7 -2
  59. data/lib/openc3/topics/system_events_topic.rb +9 -6
  60. data/lib/openc3/topics/telemetry_decom_topic.rb +1 -1
  61. data/lib/openc3/topics/telemetry_topic.rb +3 -2
  62. data/lib/openc3/topics/topic.rb +162 -3
  63. data/lib/openc3/utilities/bucket.rb +5 -0
  64. data/lib/openc3/utilities/cli_generator.rb +67 -15
  65. data/lib/openc3/utilities/config_overlay.rb +35 -17
  66. data/lib/openc3/utilities/default_plugins.rb +33 -0
  67. data/lib/openc3/utilities/local_mode.rb +82 -46
  68. data/lib/openc3/utilities/metric.rb +16 -0
  69. data/lib/openc3/utilities/process_stats.rb +160 -0
  70. data/lib/openc3/utilities/pypi_url.rb +65 -0
  71. data/lib/openc3/utilities/questdb_client.rb +140 -32
  72. data/lib/openc3/utilities/running_script.rb +22 -18
  73. data/lib/openc3/utilities/secrets.rb +5 -0
  74. data/lib/openc3/utilities/store_autoload.rb +23 -4
  75. data/lib/openc3/utilities/store_queued.rb +37 -9
  76. data/lib/openc3/utilities/target_file.rb +57 -12
  77. data/lib/openc3/utilities/topic_lag_monitor.rb +193 -0
  78. data/lib/openc3/version.rb +5 -5
  79. data/templates/conversion/conversion.py +6 -2
  80. data/templates/conversion/conversion.rb +5 -0
  81. data/templates/plugin/.github/workflows/playwright.yml +62 -0
  82. data/templates/plugin/.github/workflows/release.yml +45 -0
  83. data/templates/plugin/.github/workflows/unit-tests.yml +35 -0
  84. data/templates/plugin/.gitignore +6 -0
  85. data/templates/plugin/README.md +114 -2
  86. data/templates/plugin/lib/PACKAGE/__init__.py +4 -0
  87. data/templates/plugin/plugin.gemspec +10 -1
  88. data/templates/plugin/pyproject.toml +119 -0
  89. data/templates/tool_angular/package.json +2 -2
  90. data/templates/tool_angular/pnpm-workspace.yaml +16 -0
  91. data/templates/tool_react/package.json +1 -1
  92. data/templates/tool_react/pnpm-workspace.yaml +16 -0
  93. data/templates/tool_svelte/package.json +2 -1
  94. data/templates/tool_svelte/pnpm-workspace.yaml +20 -0
  95. data/templates/tool_vue/package.json +3 -3
  96. data/templates/tool_vue/pnpm-workspace.yaml +16 -0
  97. data/templates/widget/package.json +2 -2
  98. data/templates/widget/pnpm-workspace.yaml +16 -0
  99. metadata +16 -1
@@ -119,6 +119,7 @@ module OpenC3
119
119
  # @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED
120
120
  # @param received_time [Integer, nil] Optional received time as nanoseconds since Unix epoch
121
121
  def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, stored: false, received_time: nil, manual: false, scope: $openc3_scope, token: $openc3_token)
122
+ _tlm_reject_latest(packet_name.to_s.upcase, 'inject_tlm')
122
123
  authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
123
124
  type = type.to_s.intern
124
125
  target_name = target_name.upcase
@@ -174,12 +175,17 @@ module OpenC3
174
175
  #
175
176
  # @param args The args must either be a string followed by a value or
176
177
  # three strings followed by a value (see the calling style in the
177
- # description).
178
+ # description). A packet name of LATEST overrides the item in every
179
+ # packet which contains it.
178
180
  # @param type [Symbol] Telemetry type, :ALL (default), :RAW, :CONVERTED, :FORMATTED
179
181
  def override_tlm(*args, type: :ALL, manual: false, scope: $openc3_scope, token: $openc3_token)
180
- target_name, packet_name, item_name, value = _set_tlm_process_args(args, __method__, scope: scope)
181
- authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
182
- CvtModel.override(target_name, packet_name, item_name, value, type: type.intern, scope: scope)
182
+ target_name, packet_names, item_name, value = _set_tlm_process_args_all_packets(args, __method__, scope: scope)
183
+ packet_names.each do |packet_name|
184
+ authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
185
+ end
186
+ packet_names.each do |packet_name|
187
+ CvtModel.override(target_name, packet_name, item_name, value, type: type.intern, scope: scope)
188
+ end
183
189
  end
184
190
 
185
191
  # Get the list of CVT overrides
@@ -198,13 +204,18 @@ module OpenC3
198
204
  # Favor the first syntax where possible as it is more succinct.
199
205
  #
200
206
  # @param args The args must either be a string or three strings
201
- # (see the calling style in the description).
207
+ # (see the calling style in the description). A packet name of LATEST
208
+ # normalizes the item in every packet which contains it.
202
209
  # @param type [Symbol] Telemetry type, :ALL (default), :RAW, :CONVERTED, :FORMATTED
203
210
  # Also takes :ALL which means to normalize all telemetry types
204
211
  def normalize_tlm(*args, type: :ALL, manual: false, scope: $openc3_scope, token: $openc3_token)
205
- target_name, packet_name, item_name = _tlm_process_args(args, __method__, scope: scope)
206
- authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
207
- CvtModel.normalize(target_name, packet_name, item_name, type: type.intern, scope: scope)
212
+ target_name, packet_names, item_name = _tlm_process_args_all_packets(args, __method__, scope: scope)
213
+ packet_names.each do |packet_name|
214
+ authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
215
+ end
216
+ packet_names.each do |packet_name|
217
+ CvtModel.normalize(target_name, packet_name, item_name, type: type.intern, scope: scope)
218
+ end
208
219
  end
209
220
 
210
221
  # Returns the raw buffer for a telemetry packet.
@@ -265,45 +276,25 @@ module OpenC3
265
276
  packet_name = orig_packet_name
266
277
  raise ArgumentError, "items must be formatted as TGT__PKT__ITEM__TYPE" if target_name.nil? || packet_name.nil? || item_name.nil? || value_type.nil?
267
278
  if orig_packet_name == 'LATEST'
268
- # TODO: Do we need to lookup ALL the possible packets for this item?
279
+ # A LATEST item can come from any packet containing it. Report the best value
280
+ # type and limits of any of them since the CVT falls back to the available
281
+ # type and historical lookups check the type available in each packet.
269
282
  # We can have a large cache_timeout of 1 because all we're trying to do is lookup a packet
270
283
  packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: 1, scope: scope)
284
+ packet_names = _tlm_packet_names(target_name, orig_packet_name, item_name, scope: scope)
285
+ else
286
+ packet_names = [packet_name]
287
+ end
288
+ # Authorize every packet whose definition is read below, not just the latest one
289
+ (packet_names | [packet_name]).each do |name|
290
+ authorize(permission: 'tlm', target_name: target_name, packet_name: name, scope: scope, token: token)
271
291
  end
272
- authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token)
273
292
  begin
274
- item = TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)
275
- if Packet::RESERVED_ITEM_NAMES.include?(item_name)
276
- value_type = 'RAW' # Must request the raw value when dealing with the reserved items
277
- end
278
-
279
- # Arrays must be accessed as RAW since there's no conversion
280
- if item['array_size']
281
- value_type = 'RAW'
282
- end
283
-
284
- case value_type
285
- when 'FORMATTED', 'WITH_UNITS'
286
- if item['format_string'] or item['units']
287
- results << [target_name, orig_packet_name, item_name, 'FORMATTED'].join('__')
288
- # This logic must match the logic in Packet#decom
289
- elsif item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
290
- results << [target_name, orig_packet_name, item_name, 'CONVERTED'].join('__')
291
- else
292
- results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
293
- end
294
- when 'CONVERTED'
295
- # This logic must match the logic in Packet#decom
296
- if item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
297
- results << [target_name, orig_packet_name, item_name, 'CONVERTED'].join('__')
298
- else
299
- results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
300
- end
301
- else # RAW or unknown
302
- results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
303
- end
304
-
293
+ packet_items = packet_names.map { |name| TargetModel.packet_item(target_name, name, item_name, scope: scope) }
294
+ available_type = packet_items.map { |packet_item| _tlm_available_value_type(packet_item, item_name, value_type) }.max_by { |type| VALUE_TYPE_ORDER.index(type) }
295
+ results << [target_name, orig_packet_name, item_name, available_type].join('__')
305
296
  # Tack on __LIMITS to notify that we have an available limits value
306
- if item['limits']['DEFAULT']
297
+ if packet_items.any? { |packet_item| packet_item['limits']['DEFAULT'] }
307
298
  results[-1] += '__LIMITS'
308
299
  end
309
300
  rescue RuntimeError
@@ -341,7 +332,20 @@ module OpenC3
341
332
  parts = item.to_s.upcase.split('__')
342
333
  raise ArgumentError, "items must be formatted as TGT__PKT__ITEM__TYPE" if parts.length < 4 || parts.length > 5
343
334
  target_name, packet_name, item_name, value_type, limits = parts
344
- if packet_name == 'LATEST' # Lookup packet_name in case of LATEST
335
+ if packet_name == 'LATEST'
336
+ if start_time
337
+ # The packet received most recently now isn't necessarily the one received most
338
+ # recently at start_time so keep every candidate packet for _tlm_latest_history
339
+ # Each packet must be queried for the value type and limits it has or the query fails
340
+ candidates = _tlm_packet_names(target_name, packet_name, item_name, scope: scope).map do |candidate|
341
+ packets << [target_name, candidate]
342
+ candidate_item = TargetModel.packet_item(target_name, candidate, item_name, scope: scope)
343
+ candidate_limits = (limits and candidate_item['limits']['DEFAULT']) ? limits : nil
344
+ [target_name, candidate, item_name, _tlm_available_value_type(candidate_item, item_name, value_type), candidate_limits]
345
+ end
346
+ cvt_items << [target_name, packet_name, item_name, value_type, limits, candidates]
347
+ next
348
+ end
345
349
  packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope)
346
350
  end
347
351
  # NOTE: limits is required by the historical (start_time) QuestDB lookup
@@ -352,6 +356,9 @@ module OpenC3
352
356
  packets.each do |target_name, packet_name|
353
357
  authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
354
358
  end
359
+ if cvt_items.any? { |cvt_item| cvt_item.length == 6 }
360
+ return _tlm_latest_history(cvt_items, start_time: start_time, end_time: end_time, cache_timeout: cache_timeout, scope: scope)
361
+ end
355
362
  CvtModel.get_tlm_values(cvt_items, stale_time: stale_time, cache_timeout: cache_timeout, start_time: start_time, end_time: end_time, scope: scope)
356
363
  end
357
364
 
@@ -373,18 +380,14 @@ module OpenC3
373
380
  # @param target_name [String] Name of the target
374
381
  # @return [Array<String>] Array of all telemetry packet names
375
382
  def get_all_tlm_names(target_name, hidden: false, manual: false, scope: $openc3_scope, token: $openc3_token)
383
+ target_name = target_name.upcase
384
+ authorize(permission: 'tlm', target_name: target_name, manual: manual, scope: scope, token: token)
385
+ # Only the names are needed so avoid parsing every packet definition
376
386
  begin
377
- packets = get_all_tlm(target_name, manual: manual, scope: scope, token: token)
387
+ names = TargetModel.packet_names(target_name, type: :TLM, scope: scope)
388
+ names -= TargetModel.hidden_packet_names(target_name, type: :TLM, scope: scope) unless hidden
378
389
  rescue RuntimeError
379
- packets = []
380
- end
381
- names = []
382
- packets.each do |packet|
383
- if hidden
384
- names << packet['packet_name']
385
- else
386
- names << packet['packet_name'] unless packet['hidden']
387
- end
390
+ names = []
388
391
  end
389
392
  return names
390
393
  end
@@ -450,6 +453,7 @@ module OpenC3
450
453
  packets.each do |target_name, packet_name|
451
454
  target_name = target_name.upcase
452
455
  packet_name = packet_name.upcase
456
+ _tlm_reject_latest(packet_name, 'subscribe_packets')
453
457
  authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
454
458
  topic = "#{scope}__DECOM__{#{target_name}}__#{packet_name}"
455
459
  db_shard = Store.db_shard_for_target(target_name, scope: scope)
@@ -479,7 +483,7 @@ module OpenC3
479
483
  # Group topics by db_shard for multi-shard support
480
484
  db_shard_groups = {}
481
485
  lookup.each do |topic, offset|
482
- target_name = topic.match(/__\{?([^}_]+)\}?__/)[1] rescue nil
486
+ target_name = Topic.target_name_from_topic(topic)
483
487
  db_shard = Store.db_shard_for_target(target_name, scope: scope)
484
488
  db_shard_groups[db_shard] ||= { topics: [], offsets: [] }
485
489
  db_shard_groups[db_shard][:topics] << topic
@@ -555,9 +559,15 @@ module OpenC3
555
559
  if target_name.nil? or packet_name.nil?
556
560
  raise "ERROR: Both target name and packet name required. Usage: #{method_name}(\"TGT PKT\") or #{method_name}(\"TGT\", \"PKT\")"
557
561
  end
562
+ _tlm_reject_latest(packet_name, method_name)
558
563
  return [target_name, packet_name]
559
564
  end
560
565
 
566
+ # LATEST picks a packet per item so it can't identify a whole packet
567
+ def _tlm_reject_latest(packet_name, method_name)
568
+ raise "ERROR: LATEST is not supported by #{method_name}(), specify a packet name" if packet_name == 'LATEST'
569
+ end
570
+
561
571
  def _extract_target_packet_item_names(method_name, *args)
562
572
  target_name = nil
563
573
  packet_name = nil
@@ -593,6 +603,31 @@ module OpenC3
593
603
  end
594
604
 
595
605
  def _tlm_process_args(args, method_name, cache_timeout: nil, scope: $openc3_scope, token: $openc3_token)
606
+ target_name, packet_name, item_name = _tlm_extract_args(args, method_name)
607
+ packet_name = _tlm_resolve_packet_name(target_name, packet_name, item_name, cache_timeout: cache_timeout, scope: scope)
608
+ return [target_name, packet_name, item_name]
609
+ end
610
+
611
+ def _set_tlm_process_args(args, method_name, cache_timeout: nil, scope: $openc3_scope, token: $openc3_token)
612
+ target_name, packet_name, item_name, value = _set_tlm_extract_args(args, method_name)
613
+ packet_name = _tlm_resolve_packet_name(target_name, packet_name, item_name, cache_timeout: cache_timeout, scope: scope)
614
+ return [target_name, packet_name, item_name, value]
615
+ end
616
+
617
+ # Like _tlm_process_args but returns an array of packet names where LATEST
618
+ # expands to every packet containing the item. Used by APIs which change
619
+ # an item so the change applies wherever the item appears.
620
+ def _tlm_process_args_all_packets(args, method_name, scope: $openc3_scope)
621
+ target_name, packet_name, item_name = _tlm_extract_args(args, method_name)
622
+ return [target_name, _tlm_packet_names(target_name, packet_name, item_name, scope: scope), item_name]
623
+ end
624
+
625
+ def _set_tlm_process_args_all_packets(args, method_name, scope: $openc3_scope)
626
+ target_name, packet_name, item_name, value = _set_tlm_extract_args(args, method_name)
627
+ return [target_name, _tlm_packet_names(target_name, packet_name, item_name, scope: scope), item_name, value]
628
+ end
629
+
630
+ def _tlm_extract_args(args, method_name)
596
631
  case args.length
597
632
  when 1
598
633
  target_name, packet_name, item_name = extract_fields_from_tlm_text(args[0])
@@ -604,21 +639,10 @@ module OpenC3
604
639
  # Invalid number of arguments
605
640
  raise "ERROR: Invalid number of arguments (#{args.length}) passed to #{method_name}()"
606
641
  end
607
- target_name = target_name.upcase
608
- packet_name = packet_name.upcase
609
- item_name = item_name.upcase
610
-
611
- if packet_name == 'LATEST'
612
- packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope)
613
- else
614
- # Determine if this item exists, it will raise appropriate errors if not
615
- TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)
616
- end
617
-
618
- return [target_name, packet_name, item_name]
642
+ return [target_name.upcase, packet_name.upcase, item_name.upcase]
619
643
  end
620
644
 
621
- def _set_tlm_process_args(args, method_name, cache_timeout: nil, scope: $openc3_scope, token: $openc3_token)
645
+ def _set_tlm_extract_args(args, method_name)
622
646
  case args.length
623
647
  when 1
624
648
  target_name, packet_name, item_name, value = extract_fields_from_set_tlm_text(args[0])
@@ -631,18 +655,148 @@ module OpenC3
631
655
  # Invalid number of arguments
632
656
  raise "ERROR: Invalid number of arguments (#{args.length}) passed to #{method_name}()"
633
657
  end
634
- target_name = target_name.upcase
635
- packet_name = packet_name.upcase
636
- item_name = item_name.upcase
658
+ return [target_name.upcase, packet_name.upcase, item_name.upcase, value]
659
+ end
637
660
 
661
+ # Resolve LATEST to the most recently received packet containing the item
662
+ def _tlm_resolve_packet_name(target_name, packet_name, item_name, cache_timeout: nil, scope: $openc3_scope)
638
663
  if packet_name == 'LATEST'
639
664
  packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope)
640
665
  else
641
666
  # Determine if this item exists, it will raise appropriate errors if not
642
667
  TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)
643
668
  end
669
+ return packet_name
670
+ end
644
671
 
645
- return [target_name, packet_name, item_name, value]
672
+ VALUE_TYPE_ORDER = %w(RAW CONVERTED FORMATTED).freeze
673
+
674
+ # Returns the value type available for an item given the requested value type
675
+ def _tlm_available_value_type(item, item_name, value_type)
676
+ # Must request the raw value when dealing with the reserved items
677
+ return 'RAW' if Packet::RESERVED_ITEM_NAMES.include?(item_name)
678
+ # Arrays must be accessed as RAW since there's no conversion
679
+ return 'RAW' if item['array_size']
680
+
681
+ # This logic must match the logic in Packet#decom
682
+ converted = (item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED'))
683
+ case value_type
684
+ when 'FORMATTED', 'WITH_UNITS'
685
+ return 'FORMATTED' if item['format_string'] or item['units']
686
+ return converted ? 'CONVERTED' : 'RAW'
687
+ when 'CONVERTED'
688
+ return converted ? 'CONVERTED' : 'RAW'
689
+ else # RAW or unknown
690
+ return 'RAW'
691
+ end
692
+ end
693
+
694
+ # Historical lookup where some items are LATEST. Those items have a 6th element
695
+ # holding the item to look up in every packet which contains the item.
696
+ def _tlm_latest_history(cvt_items, start_time:, end_time:, cache_timeout:, scope:)
697
+ latest = cvt_items.each_index.select { |index| cvt_items[index].length == 6 }
698
+ regular = cvt_items.each_index.reject { |index| cvt_items[index].length == 6 }
699
+
700
+ # The QuestDB lookup ASOF JOINs every packet to the rows of the first, so
701
+ # a range needs a regular item first to drive the rows. Without one keep
702
+ # the realtime behavior of using the packet received most recently now.
703
+ if end_time and regular.all? { |index| cvt_items[index][2].nil? }
704
+ cvt_items = cvt_items.map do |target_name, packet_name, item_name, value_type, limits, candidates|
705
+ next [target_name, packet_name, item_name, value_type, limits] unless packet_name == 'LATEST'
706
+ packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope)
707
+ # Use the candidate so the query asks for the value type and limits that packet has
708
+ candidates.find { |candidate| candidate[1] == packet_name } || [target_name, packet_name, item_name, value_type, limits]
709
+ end
710
+ return CvtModel.tsdb_lookup(cvt_items, start_time: start_time, end_time: end_time, scope: scope)
711
+ end
712
+
713
+ # A packet without a table fails the query so skip those candidates
714
+ packets = latest.flat_map { |index| cvt_items[index][5].map { |candidate| candidate[0..1] } }.uniq
715
+ existing = CvtModel.tsdb_packets_with_tables(packets, scope: scope)
716
+ cvt_items = cvt_items.each_with_index.map do |cvt_item, index|
717
+ next cvt_item unless latest.include?(index)
718
+ cvt_item[0..4] + [cvt_item[5].select { |candidate| existing.include?(candidate[0..1]) }]
719
+ end
720
+
721
+ if end_time
722
+ query = []
723
+ query_index = {}
724
+ add = lambda { |query_item| query_index[query_item] ||= (query << query_item; query.length - 1) }
725
+ positions = Array.new(cvt_items.length)
726
+ regular.each { |index| positions[index] = add.call(cvt_items[index]) }
727
+ latest.each do |index|
728
+ positions[index] = cvt_items[index][5].map do |candidate|
729
+ target_name, packet_name = candidate
730
+ [add.call(candidate), add.call([target_name, packet_name, 'PACKET_TIMESECONDS', 'RAW', nil])]
731
+ end
732
+ end
733
+ rows = CvtModel.tsdb_lookup(query, start_time: start_time, end_time: end_time, scope: scope, flatten: false)
734
+ return rows unless rows.is_a?(Array) and rows.length > 0
735
+ rows = rows.map do |row|
736
+ positions.map do |position|
737
+ next row[position] if position.is_a?(Integer)
738
+ # Take the candidate with the newest packet time at this row
739
+ _tlm_newest(position.map { |value_index, time_index| [row[time_index][0], row[value_index]] }) || [nil, nil]
740
+ end
741
+ end
742
+ # Match tsdb_lookup which returns a flat array for a single row
743
+ return rows.length == 1 ? rows[0] : rows
744
+ end
745
+
746
+ # A point in time returns the last row of the first packet before start_time
747
+ # with the other packets ASOF JOINed at that row. A candidate packet received
748
+ # after that row would be missed so query each candidate packet on its own.
749
+ found = false
750
+ row = Array.new(cvt_items.length) { [nil, nil] }
751
+ unless regular.all? { |index| cvt_items[index][2].nil? }
752
+ result = CvtModel.tsdb_lookup(regular.map { |index| cvt_items[index] }, start_time: start_time, scope: scope, flatten: false)
753
+ if result.is_a?(Array) and result.length > 0
754
+ found = true
755
+ regular.each_with_index { |index, result_index| row[index] = result[0][result_index] }
756
+ end
757
+ end
758
+ # Group the LATEST items by candidate packet so each packet is queried once
759
+ by_packet = {}
760
+ latest.each do |index|
761
+ cvt_items[index][5].each do |candidate|
762
+ target_name, packet_name = candidate
763
+ (by_packet[[target_name, packet_name]] ||= {})[index] = candidate
764
+ end
765
+ end
766
+ found_values = Hash.new { |hash, key| hash[key] = [] }
767
+ by_packet.each do |(target_name, packet_name), packet_items|
768
+ query = packet_items.values.uniq
769
+ query << [target_name, packet_name, 'PACKET_TIMESECONDS', 'RAW', nil]
770
+ result = CvtModel.tsdb_lookup(query, start_time: start_time, scope: scope, flatten: false)
771
+ next unless result.is_a?(Array) and result.length > 0
772
+ values = result[0]
773
+ packet_items.each { |index, query_item| found_values[index] << [values[-1][0], values[query.index(query_item)]] }
774
+ end
775
+ found_values.each do |index, candidates|
776
+ value = _tlm_newest(candidates)
777
+ next unless value
778
+ found = true
779
+ row[index] = value
780
+ end
781
+ return found ? row : {}
782
+ end
783
+
784
+ # Return the value with the newest time from [time, value] pairs, or nil
785
+ def _tlm_newest(candidates)
786
+ candidates.select { |time, _| time }.max_by { |time, _| time }&.last
787
+ end
788
+
789
+ # Expand LATEST to every packet containing the item
790
+ def _tlm_packet_names(target_name, packet_name, item_name, scope: $openc3_scope)
791
+ if packet_name == 'LATEST'
792
+ packet_names = TargetModel.get_item_to_packet_map(target_name, scope: scope)[item_name]
793
+ raise "Item '#{target_name} LATEST #{item_name}' does not exist for scope: #{scope}" unless packet_names
794
+ return packet_names
795
+ else
796
+ # Determine if this item exists, it will raise appropriate errors if not
797
+ TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)
798
+ return [packet_name]
799
+ end
646
800
  end
647
801
  end
648
802
  end
@@ -319,8 +319,10 @@ module OpenC3
319
319
  # primitive. All other values are simply returned.
320
320
  #
321
321
  # @param value [Object]
322
+ # @param default [Object] Returned if value is nil
322
323
  # @return [true|false|Object]
323
- def self.handle_true_false(value)
324
+ def self.handle_true_false(value, default: nil)
325
+ return default if value.nil?
324
326
  if String === value
325
327
  case value.upcase
326
328
  when 'TRUE'
@@ -15,4 +15,6 @@ require 'tempfile'
15
15
 
16
16
  class Tempfile
17
17
  attr_accessor :filename
18
+ # Set by open_bucket_dialog to identify where the file came from
19
+ attr_accessor :bucket, :bucket_path
18
20
  end
@@ -76,6 +76,9 @@ module OpenC3
76
76
 
77
77
  def connect
78
78
  super() # Reset the protocols
79
+ # Discard a disconnect marker left over from a previous connection. Entries only
80
+ # wake read_interface, which finds files by listing the folder, so none are lost.
81
+ @queue.clear
79
82
 
80
83
  if @telemetry_read_folder
81
84
  @listener = Listen.to(@telemetry_read_folder, force_polling: @polling) do |modified, added, removed|
@@ -96,7 +99,9 @@ module OpenC3
96
99
  @sleeper.cancel if @sleeper
97
100
  @listener.stop if @listener
98
101
  @listener = nil
99
- @queue << nil
102
+ # Only wake read_interface for the first disconnect of a connection, otherwise the
103
+ # unconsumed marker would immediately disconnect the next connection
104
+ @queue << nil if @connected
100
105
  super()
101
106
  @connected = false
102
107
  end
@@ -51,6 +51,10 @@ module OpenC3
51
51
 
52
52
  # Connects the interface to its target(s)
53
53
  def connect
54
+ # Discard anything still queued from a previous connection, such as a response to a
55
+ # request that completed after disconnect. A request still in flight across the
56
+ # reconnect can queue its response after this; that is a real response and is read normally.
57
+ @response_queue.clear
54
58
  # Per https://github.com/lostisland/faraday/blob/main/lib/faraday/options/env.rb
55
59
  # :timeout - time limit for the entire request (Integer in seconds)
56
60
  # :open_timeout - time limit for just the connection phase (e.g. handshake) (Integer in seconds)
@@ -79,15 +83,19 @@ module OpenC3
79
83
 
80
84
  # Disconnects the interface from its target(s)
81
85
  def disconnect
86
+ was_connected = !@http.nil?
82
87
  @http.close if @http
83
88
  @http = nil
84
- # Clear the response queue
85
- while @response_queue.length > 0
86
- @response_queue.pop
87
- end
88
89
  super()
89
- # Push nil to cause read_interface to return nil
90
- @response_queue.push(nil)
90
+ # disconnect is called more than once per connection loss (e.g. again after
91
+ # read_interface returns nil), so only the first call queues the marker. Another
92
+ # marker would be left unconsumed and immediately disconnect the next connection,
93
+ # and clearing again could discard a marker the reader hasn't consumed yet.
94
+ if was_connected
95
+ @response_queue.clear
96
+ # Push nil to cause read_interface to return nil
97
+ @response_queue.push(nil)
98
+ end
91
99
  end
92
100
 
93
101
  # Called to convert a packet into a data buffer. Write protocols then
@@ -70,6 +70,9 @@ module OpenC3
70
70
  # this interface
71
71
  attr_accessor :disable_disconnect
72
72
 
73
+ # @return [Boolean] Flag indicating if the interface is allowed to read
74
+ attr_writer :read_allowed
75
+
73
76
  # @return [StreamLogPair] StreamLogPair instance or nil
74
77
  attr_accessor :stream_log_pair
75
78
 
@@ -326,6 +329,12 @@ module OpenC3
326
329
  # Return packet
327
330
  @read_count += 1
328
331
  Logger.warn("#{@name}: Interface unexpectedly requested disconnect") unless packet
332
+ # Timestamp buffered reads with when the data arrived rather than when
333
+ # it was processed (otherwise received_time is set when it is handled)
334
+ if packet and !packet.received_time
335
+ data_time = read_queue_data_time
336
+ packet.received_time = data_time if data_time
337
+ end
329
338
  return packet
330
339
  end
331
340
  rescue Exception => e
@@ -349,7 +358,8 @@ module OpenC3
349
358
  if packet == :DISCONNECT
350
359
  Logger.info("#{@name}: Protocol #{protocol.class} write_packet requested disconnect")
351
360
  disconnect()
352
- return
361
+ # Nothing was written so reject rather than report success
362
+ raise WriteRejectError.new("Protocol #{protocol.class} write_packet requested disconnect")
353
363
  end
354
364
  return if packet == :STOP
355
365
  end
@@ -369,7 +379,8 @@ module OpenC3
369
379
  if data == :DISCONNECT
370
380
  Logger.info("#{@name}: Protocol #{protocol.class} write_data requested disconnect")
371
381
  disconnect()
372
- return
382
+ # Nothing was written so reject rather than report success
383
+ raise WriteRejectError.new("Protocol #{protocol.class} write_data requested disconnect")
373
384
  end
374
385
  return if data == :STOP
375
386
  end
@@ -443,6 +454,19 @@ module OpenC3
443
454
  config
444
455
  end
445
456
 
457
+ # @return [Integer] The number of bytes waiting on the read queue.
458
+ # Interfaces which buffer raw reads override this (see ReadQueueInterface).
459
+ def read_queue_bytes
460
+ 0
461
+ end
462
+
463
+ # @return [Time, nil] When the data most recently returned by
464
+ # read_interface was read from the underlying source, or nil if unknown.
465
+ # Interfaces which buffer raw reads override this (see ReadQueueInterface).
466
+ def read_queue_data_time
467
+ nil
468
+ end
469
+
446
470
  # @return [Boolean] Whether reading is allowed
447
471
  def read_allowed?
448
472
  @read_allowed
@@ -564,7 +588,7 @@ module OpenC3
564
588
  # @return [String] Raw packet data
565
589
  def read_interface_base(data, _extra = nil)
566
590
  if @save_raw_data
567
- @read_raw_data_time = Time.now
591
+ @read_raw_data_time = read_queue_data_time || Time.now
568
592
  @read_raw_data = data.clone
569
593
  end
570
594
  @bytes_read += data.length