openc3 7.5.0 → 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.
- checksums.yaml +4 -4
- data/lib/openc3/api/limits_api.rb +149 -160
- data/lib/openc3/api/tlm_api.rb +219 -61
- data/lib/openc3/logs/log_writer.rb +9 -8
- data/lib/openc3/microservices/decom_microservice.rb +23 -1
- data/lib/openc3/microservices/interface_decom_common.rb +3 -1
- data/lib/openc3/models/cvt_model.rb +7 -2
- data/lib/openc3/models/target_model.rb +9 -6
- data/lib/openc3/packets/packet_item.rb +13 -8
- data/lib/openc3/script/api_shared.rb +10 -8
- data/lib/openc3/script/extract.rb +52 -5
- data/lib/openc3/script/script.rb +1 -1
- data/lib/openc3/topics/limits_event_topic.rb +90 -3
- data/lib/openc3/topics/router_topic.rb +6 -2
- data/lib/openc3/topics/system_events_topic.rb +9 -6
- data/lib/openc3/topics/topic.rb +8 -1
- data/lib/openc3/utilities/bucket.rb +5 -0
- data/lib/openc3/utilities/questdb_client.rb +126 -32
- data/lib/openc3/utilities/secrets.rb +5 -0
- data/lib/openc3/utilities/store_autoload.rb +3 -1
- data/lib/openc3/version.rb +5 -5
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -3
- data/templates/widget/package.json +2 -2
- metadata +1 -1
data/lib/openc3/api/tlm_api.rb
CHANGED
|
@@ -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,
|
|
181
|
-
|
|
182
|
-
|
|
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,
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
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'
|
|
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
|
|
|
@@ -446,6 +453,7 @@ module OpenC3
|
|
|
446
453
|
packets.each do |target_name, packet_name|
|
|
447
454
|
target_name = target_name.upcase
|
|
448
455
|
packet_name = packet_name.upcase
|
|
456
|
+
_tlm_reject_latest(packet_name, 'subscribe_packets')
|
|
449
457
|
authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
|
|
450
458
|
topic = "#{scope}__DECOM__{#{target_name}}__#{packet_name}"
|
|
451
459
|
db_shard = Store.db_shard_for_target(target_name, scope: scope)
|
|
@@ -475,7 +483,7 @@ module OpenC3
|
|
|
475
483
|
# Group topics by db_shard for multi-shard support
|
|
476
484
|
db_shard_groups = {}
|
|
477
485
|
lookup.each do |topic, offset|
|
|
478
|
-
target_name =
|
|
486
|
+
target_name = Topic.target_name_from_topic(topic)
|
|
479
487
|
db_shard = Store.db_shard_for_target(target_name, scope: scope)
|
|
480
488
|
db_shard_groups[db_shard] ||= { topics: [], offsets: [] }
|
|
481
489
|
db_shard_groups[db_shard][:topics] << topic
|
|
@@ -551,9 +559,15 @@ module OpenC3
|
|
|
551
559
|
if target_name.nil? or packet_name.nil?
|
|
552
560
|
raise "ERROR: Both target name and packet name required. Usage: #{method_name}(\"TGT PKT\") or #{method_name}(\"TGT\", \"PKT\")"
|
|
553
561
|
end
|
|
562
|
+
_tlm_reject_latest(packet_name, method_name)
|
|
554
563
|
return [target_name, packet_name]
|
|
555
564
|
end
|
|
556
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
|
+
|
|
557
571
|
def _extract_target_packet_item_names(method_name, *args)
|
|
558
572
|
target_name = nil
|
|
559
573
|
packet_name = nil
|
|
@@ -589,6 +603,31 @@ module OpenC3
|
|
|
589
603
|
end
|
|
590
604
|
|
|
591
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)
|
|
592
631
|
case args.length
|
|
593
632
|
when 1
|
|
594
633
|
target_name, packet_name, item_name = extract_fields_from_tlm_text(args[0])
|
|
@@ -600,21 +639,10 @@ module OpenC3
|
|
|
600
639
|
# Invalid number of arguments
|
|
601
640
|
raise "ERROR: Invalid number of arguments (#{args.length}) passed to #{method_name}()"
|
|
602
641
|
end
|
|
603
|
-
target_name
|
|
604
|
-
packet_name = packet_name.upcase
|
|
605
|
-
item_name = item_name.upcase
|
|
606
|
-
|
|
607
|
-
if packet_name == 'LATEST'
|
|
608
|
-
packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope)
|
|
609
|
-
else
|
|
610
|
-
# Determine if this item exists, it will raise appropriate errors if not
|
|
611
|
-
TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)
|
|
612
|
-
end
|
|
613
|
-
|
|
614
|
-
return [target_name, packet_name, item_name]
|
|
642
|
+
return [target_name.upcase, packet_name.upcase, item_name.upcase]
|
|
615
643
|
end
|
|
616
644
|
|
|
617
|
-
def
|
|
645
|
+
def _set_tlm_extract_args(args, method_name)
|
|
618
646
|
case args.length
|
|
619
647
|
when 1
|
|
620
648
|
target_name, packet_name, item_name, value = extract_fields_from_set_tlm_text(args[0])
|
|
@@ -627,18 +655,148 @@ module OpenC3
|
|
|
627
655
|
# Invalid number of arguments
|
|
628
656
|
raise "ERROR: Invalid number of arguments (#{args.length}) passed to #{method_name}()"
|
|
629
657
|
end
|
|
630
|
-
target_name
|
|
631
|
-
|
|
632
|
-
item_name = item_name.upcase
|
|
658
|
+
return [target_name.upcase, packet_name.upcase, item_name.upcase, value]
|
|
659
|
+
end
|
|
633
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)
|
|
634
663
|
if packet_name == 'LATEST'
|
|
635
664
|
packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope)
|
|
636
665
|
else
|
|
637
666
|
# Determine if this item exists, it will raise appropriate errors if not
|
|
638
667
|
TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)
|
|
639
668
|
end
|
|
669
|
+
return packet_name
|
|
670
|
+
end
|
|
640
671
|
|
|
641
|
-
|
|
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
|
|
642
800
|
end
|
|
643
801
|
end
|
|
644
802
|
end
|
|
@@ -80,14 +80,6 @@ module OpenC3
|
|
|
80
80
|
# Sleeper used to delay cycle thread
|
|
81
81
|
@@cycle_sleeper = nil
|
|
82
82
|
|
|
83
|
-
# Returns the Redis db_shard holding a target stream such as
|
|
84
|
-
# SCOPE__TELEMETRY__{TARGET}__PACKET, or 0 if no target can be determined
|
|
85
|
-
def self.db_shard_for_topic(redis_topic)
|
|
86
|
-
target_match = redis_topic.match(/\{([^{}]+)\}/)
|
|
87
|
-
return 0 unless target_match
|
|
88
|
-
Store.db_shard_for_target(target_match[1], scope: redis_topic.split('__')[0])
|
|
89
|
-
end
|
|
90
|
-
|
|
91
83
|
# @param remote_log_directory [String] The path to store the log files
|
|
92
84
|
# @param logging_enabled [Boolean] Whether to start with logging enabled
|
|
93
85
|
# @param cycle_time [Integer] The amount of time in seconds before creating
|
|
@@ -222,6 +214,15 @@ module OpenC3
|
|
|
222
214
|
end
|
|
223
215
|
end
|
|
224
216
|
|
|
217
|
+
# The db_shard of a target topic like DEFAULT__DECOM__{INST}__HEALTH_STATUS
|
|
218
|
+
# or db_shard 0 for a topic without a target
|
|
219
|
+
def self.db_shard_for_topic(redis_topic)
|
|
220
|
+
scope, _, rest = redis_topic.delete_prefix('{').partition('__')
|
|
221
|
+
# The target is a {TARGET} part between two others, e.g. DECOM__{INST}__HEALTH_STATUS
|
|
222
|
+
target = rest.split('__', -1)[1...-1]&.find { |part| part.length > 2 and part.start_with?('{') and part.end_with?('}') }
|
|
223
|
+
(!scope.empty? and target) ? Store.db_shard_for_target(target[1...-1], scope: scope) : 0
|
|
224
|
+
end
|
|
225
|
+
|
|
225
226
|
def cycle_thread_body
|
|
226
227
|
@@cycle_sleeper = Sleeper.new
|
|
227
228
|
while true
|
|
@@ -99,6 +99,10 @@ module OpenC3
|
|
|
99
99
|
@limits_event_topic = "#{@scope}__openc3_limits_events"
|
|
100
100
|
@topics << @limits_event_topic
|
|
101
101
|
Topic.update_topic_offsets(@topics, db_shard: @db_shard)
|
|
102
|
+
# Last limits event read, used to detect events trimmed before they were read.
|
|
103
|
+
# Read from the stream rather than the thread's cached offsets. The run thread
|
|
104
|
+
# starts reading at or after this point, so it can only cause an extra resync.
|
|
105
|
+
@limits_event_offset = Topic.get_last_offset(@limits_event_topic, db_shard: @db_shard)
|
|
102
106
|
# Initialize before assigning limits_change_callback - sync_system below
|
|
103
107
|
# can fire the callback synchronously for any persisted-disabled items.
|
|
104
108
|
@limits_response_queue = Queue.new
|
|
@@ -121,14 +125,21 @@ module OpenC3
|
|
|
121
125
|
break if @cancel_thread
|
|
122
126
|
|
|
123
127
|
begin
|
|
128
|
+
limits_events_checked = false
|
|
124
129
|
OpenC3.in_span("read_topics") do
|
|
125
130
|
Topic.read_topics(@topics, db_shard: @db_shard) do |topic, msg_id, msg_hash, redis|
|
|
126
131
|
break if @cancel_thread
|
|
127
132
|
if topic == @microservice_topic
|
|
128
133
|
microservice_cmd(topic, msg_id, msg_hash, redis)
|
|
129
134
|
elsif topic == @limits_event_topic
|
|
135
|
+
# Any gap is before the first event in a read so only check once per read
|
|
136
|
+
unless limits_events_checked
|
|
137
|
+
resync_if_limits_events_trimmed()
|
|
138
|
+
limits_events_checked = true
|
|
139
|
+
end
|
|
140
|
+
@limits_event_offset = msg_id
|
|
130
141
|
event = JSON.parse(msg_hash['event'], allow_nan: true, create_additions: true)
|
|
131
|
-
LimitsEventTopic.process_event(event)
|
|
142
|
+
LimitsEventTopic.process_event(event, scope: @scope)
|
|
132
143
|
elsif topic =~ /__DECOMINTERFACE/
|
|
133
144
|
if msg_hash.key?('inject_tlm')
|
|
134
145
|
handle_inject_tlm_with_ack(msg_hash['inject_tlm'], msg_id)
|
|
@@ -161,6 +172,17 @@ module OpenC3
|
|
|
161
172
|
@limits_response_thread = nil
|
|
162
173
|
end
|
|
163
174
|
|
|
175
|
+
# Limits events are only read between batches of packets, so a decom microservice
|
|
176
|
+
# working through a backlog can fall far enough behind that a flood of LIMITS_CHANGE
|
|
177
|
+
# events trims control events (enable, settings, etc) before it reads them.
|
|
178
|
+
# Recover by reloading all limits state from the store, which is always current.
|
|
179
|
+
def resync_if_limits_events_trimmed
|
|
180
|
+
if LimitsEventTopic.events_trimmed?(@limits_event_offset, scope: @scope, db_shard: @db_shard)
|
|
181
|
+
@logger.warn("#{@name}: Limits events were trimmed before they could be read, resyncing limits")
|
|
182
|
+
LimitsEventTopic.resync_system(scope: @scope)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
164
186
|
def decom_packet(topic, msg_id, msg_hash, _redis)
|
|
165
187
|
OpenC3.in_span("decom_packet") do
|
|
166
188
|
update_topic_lag(topic, msg_id, metric_name: 'decom_topic_delta_seconds', help: 'Delta time between data written to stream and decom start')
|
|
@@ -42,6 +42,8 @@ module OpenC3
|
|
|
42
42
|
def handle_inject_tlm_with_ack(inject_tlm_json, msg_id)
|
|
43
43
|
inject_tlm_hash = JSON.parse(inject_tlm_json, allow_nan: true, create_additions: true)
|
|
44
44
|
target_name = inject_tlm_hash['target_name']
|
|
45
|
+
# Ack on the db_shard of the target where DecomInterfaceTopic.inject_tlm waits for it
|
|
46
|
+
db_shard = Store.db_shard_for_target(target_name, scope: @scope)
|
|
45
47
|
ack_topic = "{#{@scope}__ACKCMD}TARGET__#{target_name}"
|
|
46
48
|
begin
|
|
47
49
|
handle_inject_tlm(inject_tlm_json)
|
|
@@ -56,7 +58,7 @@ module OpenC3
|
|
|
56
58
|
result: error.message
|
|
57
59
|
}
|
|
58
60
|
end
|
|
59
|
-
Topic.write_topic(ack_topic, msg_hash)
|
|
61
|
+
Topic.write_topic(ack_topic, msg_hash, db_shard: db_shard)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def handle_build_cmd(build_cmd_json, msg_id)
|
|
@@ -134,8 +134,13 @@ module OpenC3
|
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
def self.tsdb_lookup(items, start_time:, end_time: nil, scope: $openc3_scope)
|
|
138
|
-
QuestDBClient.tsdb_lookup(items, start_time: start_time, end_time: end_time, scope: scope)
|
|
137
|
+
def self.tsdb_lookup(items, start_time:, end_time: nil, scope: $openc3_scope, flatten: true)
|
|
138
|
+
QuestDBClient.tsdb_lookup(items, start_time: start_time, end_time: end_time, scope: scope, flatten: flatten)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Return the [target_name, packet_name] telemetry packets which have a TSDB table
|
|
142
|
+
def self.tsdb_packets_with_tables(packets, scope: $openc3_scope)
|
|
143
|
+
QuestDBClient.tlm_packets_with_tables(packets, scope: scope)
|
|
139
144
|
end
|
|
140
145
|
|
|
141
146
|
# Return all item values and limit state from the CVT
|
|
@@ -458,10 +458,13 @@ module OpenC3
|
|
|
458
458
|
end
|
|
459
459
|
|
|
460
460
|
def self.get_item_to_packet_map(target_name, scope:)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
item_map_key = "#{scope}__#{target_name}__item_to_packet_map"
|
|
461
|
+
# Normalize before building the key so lowercase lookups share the entry
|
|
462
|
+
# that update_store_item_map and undeploy refresh and invalidate
|
|
464
463
|
target_name = target_name.upcase
|
|
464
|
+
# The cache is keyed by item_map_key since the same target can exist in multiple scopes
|
|
465
|
+
item_map_key = "#{scope}__#{target_name}__item_to_packet_map"
|
|
466
|
+
cache_time, item_map = @@item_map_cache[item_map_key]
|
|
467
|
+
return item_map if item_map and (Time.now - cache_time) < ITEM_MAP_CACHE_TIMEOUT
|
|
465
468
|
store = store_for_target(target_name, scope: scope)
|
|
466
469
|
json_data = store.get(item_map_key)
|
|
467
470
|
if json_data
|
|
@@ -470,7 +473,7 @@ module OpenC3
|
|
|
470
473
|
item_map = build_item_to_packet_map(target_name, scope: scope)
|
|
471
474
|
store.set(item_map_key, JSON.generate(item_map, allow_nan: true))
|
|
472
475
|
end
|
|
473
|
-
@@item_map_cache[
|
|
476
|
+
@@item_map_cache[item_map_key] = [Time.now, item_map]
|
|
474
477
|
return item_map
|
|
475
478
|
end
|
|
476
479
|
|
|
@@ -917,7 +920,7 @@ module OpenC3
|
|
|
917
920
|
# Delete item_map
|
|
918
921
|
item_map_key = "#{@scope}__#{@name}__item_to_packet_map"
|
|
919
922
|
db_shard_store.del(item_map_key)
|
|
920
|
-
@@item_map_cache[
|
|
923
|
+
@@item_map_cache[item_map_key] = nil
|
|
921
924
|
|
|
922
925
|
topic = { kind: 'deleted', type: 'target', name: @name }
|
|
923
926
|
# The UNKNOWN target doesn't have an associated plugin
|
|
@@ -1101,7 +1104,7 @@ module OpenC3
|
|
|
1101
1104
|
item_map_key = "#{@scope}__#{@name}__item_to_packet_map"
|
|
1102
1105
|
item_map = self.class.build_item_to_packet_map(@name, scope: @scope)
|
|
1103
1106
|
Store.instance(db_shard: @db_shard).set(item_map_key, JSON.generate(item_map, allow_nan: true))
|
|
1104
|
-
@@item_map_cache[
|
|
1107
|
+
@@item_map_cache[item_map_key] = [Time.now, item_map]
|
|
1105
1108
|
end
|
|
1106
1109
|
|
|
1107
1110
|
def update_store(system, clear_old: true)
|
|
@@ -398,14 +398,19 @@ module OpenC3
|
|
|
398
398
|
if @states
|
|
399
399
|
@states.each do |state_name, state_value|
|
|
400
400
|
config << " STATE #{state_name.to_s.quote_if_necessary} #{state_value.to_s.quote_if_necessary}"
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
401
|
+
# Only write the attributes STATE accepts for this packet type so the config parses back
|
|
402
|
+
if cmd_or_tlm == :TELEMETRY
|
|
403
|
+
if @state_colors and @state_colors[state_name]
|
|
404
|
+
config << " #{@state_colors[state_name]}"
|
|
405
|
+
end
|
|
406
|
+
else
|
|
407
|
+
if @hazardous and @hazardous[state_name]
|
|
408
|
+
config << " HAZARDOUS"
|
|
409
|
+
config << " #{@hazardous[state_name].to_s.quote_if_necessary}" unless @hazardous[state_name].to_s.empty?
|
|
410
|
+
end
|
|
411
|
+
if @messages_disabled and @messages_disabled[state_name]
|
|
412
|
+
config << " DISABLE_MESSAGES"
|
|
413
|
+
end
|
|
409
414
|
end
|
|
410
415
|
config << "\n"
|
|
411
416
|
end
|
|
@@ -37,7 +37,7 @@ module OpenC3
|
|
|
37
37
|
# @param args [String|Array<String>] See the description for calling style
|
|
38
38
|
# @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED
|
|
39
39
|
def check(*args, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token)
|
|
40
|
-
_check(*args, scope: scope) { |tgt, pkt, item| tlm(tgt, pkt, item, type: type, scope: scope, token: token) }
|
|
40
|
+
_check(*args, type: type, scope: scope, token: token) { |tgt, pkt, item| tlm(tgt, pkt, item, type: type, scope: scope, token: token) }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def check_raw(*args, scope: $openc3_scope, token: $openc3_token)
|
|
@@ -525,12 +525,12 @@ module OpenC3
|
|
|
525
525
|
# Implementation of the various check commands. It yields back to the
|
|
526
526
|
# caller to allow the return of the value through various telemetry calls.
|
|
527
527
|
# This method should not be called directly by application code.
|
|
528
|
-
def _check(*args, scope: $openc3_scope, token: $openc3_token)
|
|
528
|
+
def _check(*args, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token)
|
|
529
529
|
target_name, packet_name, item_name, comparison_to_eval = _check_process_args(args, 'check')
|
|
530
530
|
|
|
531
531
|
value = yield(target_name, packet_name, item_name)
|
|
532
532
|
if comparison_to_eval
|
|
533
|
-
_check_comparison(target_name, packet_name, item_name, comparison_to_eval, value)
|
|
533
|
+
_check_comparison(target_name, packet_name, item_name, type, comparison_to_eval, value, scope: scope, token: token)
|
|
534
534
|
else
|
|
535
535
|
puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value.nil? ? 'nil' : value.inspect}"
|
|
536
536
|
end
|
|
@@ -596,6 +596,8 @@ module OpenC3
|
|
|
596
596
|
quiet: false,
|
|
597
597
|
scope: $openc3_scope, token: $openc3_token)
|
|
598
598
|
type = (check ? 'CHECK' : 'WAIT')
|
|
599
|
+
# LATEST would compare the RECEIVED_COUNT of whichever packet was received most recently
|
|
600
|
+
raise "LATEST is not supported by wait_packet, specify a packet name" if packet_name.to_s.upcase == 'LATEST'
|
|
599
601
|
initial_count = tlm(target_name, packet_name, 'RECEIVED_COUNT', scope: scope, token: token)
|
|
600
602
|
# If the packet has not been received the initial_count could be nil
|
|
601
603
|
initial_count = 0 unless initial_count
|
|
@@ -768,10 +770,10 @@ module OpenC3
|
|
|
768
770
|
# Builds a callable which compares a telemetry value against the given comparison string,
|
|
769
771
|
# e.g. "> 1". Returns nil if there is no comparison, e.g. a block based wait_check().
|
|
770
772
|
# Raises if the comparison is invalid, e.g. an unsupported operator or unparsable operand.
|
|
771
|
-
def _comparison_implementation(comparison_to_eval)
|
|
773
|
+
def _comparison_implementation(target_name, packet_name, item_name, value_type, comparison_to_eval, scope: $openc3_scope, token: $openc3_token)
|
|
772
774
|
return nil unless comparison_to_eval
|
|
773
775
|
|
|
774
|
-
operator, operand =
|
|
776
|
+
operator, operand = extract_comparison(comparison_to_eval, value_type) { get_item(target_name, packet_name, item_name, scope: scope, token: token) }
|
|
775
777
|
return nil unless operator
|
|
776
778
|
lambda { |value| compare_values(value, operator, operand) }
|
|
777
779
|
end
|
|
@@ -783,7 +785,7 @@ module OpenC3
|
|
|
783
785
|
raise "ERROR: Invalid comparison to non-ascii value"
|
|
784
786
|
end
|
|
785
787
|
# A user supplied block is the condition so the comparison text is never parsed
|
|
786
|
-
comparison = block ? nil : _comparison_implementation(comparison_to_eval)
|
|
788
|
+
comparison = block ? nil : _comparison_implementation(target_name, packet_name, item_name, value_type, comparison_to_eval, scope: scope, token: token)
|
|
787
789
|
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
788
790
|
end
|
|
789
791
|
|
|
@@ -851,14 +853,14 @@ module OpenC3
|
|
|
851
853
|
end
|
|
852
854
|
end
|
|
853
855
|
|
|
854
|
-
def _check_comparison(target_name, packet_name, item_name, comparison_to_eval, value)
|
|
856
|
+
def _check_comparison(target_name, packet_name, item_name, value_type, comparison_to_eval, value, scope: $openc3_scope, token: $openc3_token)
|
|
855
857
|
check_str = "CHECK: #{_upcase(target_name, packet_name, item_name)} #{comparison_to_eval}"
|
|
856
858
|
# Show user the check against a quoted string
|
|
857
859
|
value_str = value.is_a?(String) ? "'#{value}'" : value
|
|
858
860
|
value_str = 'nil' if value.nil? # Show user nil value as 'nil'
|
|
859
861
|
with_value = "with value == #{value_str}"
|
|
860
862
|
|
|
861
|
-
operator, operand =
|
|
863
|
+
operator, operand = extract_comparison(comparison_to_eval, value_type) { get_item(target_name, packet_name, item_name, scope: scope, token: token) }
|
|
862
864
|
unless _valid_comparison?(value, operator, operand)
|
|
863
865
|
message = "Invalid comparison for types"
|
|
864
866
|
if $disconnect
|