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.
- checksums.yaml +4 -4
- data/bin/pipinstall +18 -5
- data/bin/pipuninstall +1 -1
- data/bin/uvinstall +36 -8
- data/data/config/default_plugins.yaml +43 -0
- data/data/config/interface_modifiers.yaml +22 -7
- data/data/config/item_modifiers.yaml +14 -4
- data/data/config/plugins.yaml +1 -1
- data/data/config/widgets.yaml +1 -1
- data/lib/openc3/api/cmd_api.rb +10 -13
- data/lib/openc3/api/interface_api.rb +1 -2
- data/lib/openc3/api/limits_api.rb +149 -160
- data/lib/openc3/api/metrics_api.rb +26 -1
- data/lib/openc3/api/router_api.rb +1 -2
- data/lib/openc3/api/tlm_api.rb +225 -71
- data/lib/openc3/config/config_parser.rb +3 -1
- data/lib/openc3/core_ext/tempfile.rb +2 -0
- data/lib/openc3/interfaces/file_interface.rb +6 -1
- data/lib/openc3/interfaces/http_client_interface.rb +14 -6
- data/lib/openc3/interfaces/interface.rb +27 -3
- data/lib/openc3/interfaces/read_queue_interface.rb +331 -0
- data/lib/openc3/interfaces/stream_interface.rb +22 -3
- data/lib/openc3/interfaces/tcpip_server_interface.rb +13 -0
- data/lib/openc3/interfaces/udp_interface.rb +16 -2
- data/lib/openc3/interfaces.rb +1 -0
- data/lib/openc3/logs/log_writer.rb +14 -4
- data/lib/openc3/microservices/decom_microservice.rb +30 -5
- data/lib/openc3/microservices/interface_decom_common.rb +3 -1
- data/lib/openc3/microservices/interface_microservice.rb +146 -50
- data/lib/openc3/microservices/log_microservice.rb +1 -3
- data/lib/openc3/microservices/microservice.rb +25 -1
- data/lib/openc3/microservices/plugin_microservice.rb +32 -0
- data/lib/openc3/microservices/text_log_microservice.rb +1 -3
- data/lib/openc3/models/cvt_model.rb +7 -2
- data/lib/openc3/models/interface_model.rb +1 -1
- data/lib/openc3/models/interface_status_model.rb +6 -0
- data/lib/openc3/models/metric_model.rb +52 -4
- data/lib/openc3/models/microservice_model.rb +7 -3
- data/lib/openc3/models/plugin_model.rb +12 -12
- data/lib/openc3/models/python_package_model.rb +3 -13
- data/lib/openc3/models/target_model.rb +92 -15
- data/lib/openc3/operators/microservice_operator.rb +5 -0
- data/lib/openc3/operators/operator.rb +69 -0
- data/lib/openc3/packets/packet.rb +10 -4
- data/lib/openc3/packets/packet_config.rb +12 -1
- data/lib/openc3/packets/packet_item.rb +40 -22
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/structure_item.rb +3 -2
- data/lib/openc3/script/api_shared.rb +62 -86
- data/lib/openc3/script/extract.rb +263 -20
- data/lib/openc3/script/plugins.rb +4 -11
- data/lib/openc3/script/script.rb +1 -1
- data/lib/openc3/script/storage.rb +81 -10
- data/lib/openc3/system/system.rb +9 -6
- data/lib/openc3/topics/command_decom_topic.rb +1 -1
- data/lib/openc3/topics/command_topic.rb +2 -1
- data/lib/openc3/topics/limits_event_topic.rb +90 -3
- data/lib/openc3/topics/router_topic.rb +7 -2
- data/lib/openc3/topics/system_events_topic.rb +9 -6
- data/lib/openc3/topics/telemetry_decom_topic.rb +1 -1
- data/lib/openc3/topics/telemetry_topic.rb +3 -2
- data/lib/openc3/topics/topic.rb +162 -3
- data/lib/openc3/utilities/bucket.rb +5 -0
- data/lib/openc3/utilities/cli_generator.rb +67 -15
- data/lib/openc3/utilities/config_overlay.rb +35 -17
- data/lib/openc3/utilities/default_plugins.rb +33 -0
- data/lib/openc3/utilities/local_mode.rb +82 -46
- data/lib/openc3/utilities/metric.rb +16 -0
- data/lib/openc3/utilities/process_stats.rb +160 -0
- data/lib/openc3/utilities/pypi_url.rb +65 -0
- data/lib/openc3/utilities/questdb_client.rb +140 -32
- data/lib/openc3/utilities/running_script.rb +22 -18
- data/lib/openc3/utilities/secrets.rb +5 -0
- data/lib/openc3/utilities/store_autoload.rb +23 -4
- data/lib/openc3/utilities/store_queued.rb +37 -9
- data/lib/openc3/utilities/target_file.rb +57 -12
- data/lib/openc3/utilities/topic_lag_monitor.rb +193 -0
- data/lib/openc3/version.rb +5 -5
- data/templates/conversion/conversion.py +6 -2
- data/templates/conversion/conversion.rb +5 -0
- data/templates/plugin/.github/workflows/playwright.yml +62 -0
- data/templates/plugin/.github/workflows/release.yml +45 -0
- data/templates/plugin/.github/workflows/unit-tests.yml +35 -0
- data/templates/plugin/.gitignore +6 -0
- data/templates/plugin/README.md +114 -2
- data/templates/plugin/lib/PACKAGE/__init__.py +4 -0
- data/templates/plugin/plugin.gemspec +10 -1
- data/templates/plugin/pyproject.toml +119 -0
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_angular/pnpm-workspace.yaml +16 -0
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_react/pnpm-workspace.yaml +16 -0
- data/templates/tool_svelte/package.json +2 -1
- data/templates/tool_svelte/pnpm-workspace.yaml +20 -0
- data/templates/tool_vue/package.json +3 -3
- data/templates/tool_vue/pnpm-workspace.yaml +16 -0
- data/templates/widget/package.json +2 -2
- data/templates/widget/pnpm-workspace.yaml +16 -0
- metadata +16 -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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
635
|
-
|
|
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
|
-
|
|
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'
|
|
@@ -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
|
-
|
|
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
|
-
#
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|