# encoding: ascii-8bit # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it # under the terms of the GNU Affero General Public License # as published by the Free Software Foundation; version 3 with # attribution addendums as found in the LICENSE.txt # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. # All changes Copyright 2022, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. require 'openc3/models/target_model' require 'openc3/models/cvt_model' require 'openc3/packets/packet' require 'openc3/topics/telemetry_topic' module OpenC3 module Api WHITELIST ||= [] WHITELIST.concat([ 'tlm', 'tlm_raw', 'tlm_formatted', 'tlm_with_units', 'tlm_variable', 'set_tlm', 'inject_tlm', 'override_tlm', 'normalize_tlm', 'get_tlm_buffer', 'get_tlm_packet', 'get_tlm_values', 'get_all_telemetry', 'get_all_telemetry_names', 'get_telemetry', 'get_item', 'subscribe_packets', 'get_packets', 'get_tlm_cnt', 'get_tlm_cnts', 'get_packet_derived_items', ]) # Request a telemetry item from a packet. # # Accepts two different calling styles: # tlm("TGT PKT ITEM") # tlm('TGT','PKT','ITEM') # # Favor the first syntax where possible as it is more succinct. # # @param args [String|Array] See the description for calling style # @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS # @return [Object] The telemetry value formatted as requested def tlm(*args, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token) target_name, packet_name, item_name = tlm_process_args(args, 'tlm', scope: scope) authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) CvtModel.get_item(target_name, packet_name, item_name, type: type.intern, scope: scope) end # @deprecated Use tlm with type: :RAW def tlm_raw(*args, scope: $openc3_scope, token: $openc3_token) tlm(*args, type: :RAW, scope: scope, token: token) end # @deprecated Use tlm with type: :FORMATTED def tlm_formatted(*args, scope: $openc3_scope, token: $openc3_token) tlm(*args, type: :FORMATTED, scope: scope, token: token) end # @deprecated Use tlm with type: :WITH_UNITS def tlm_with_units(*args, scope: $openc3_scope, token: $openc3_token) tlm(*args, type: :WITH_UNITS, scope: scope, token: token) end # @deprecated Use tlm with type: def tlm_variable(*args, scope: $openc3_scope, token: $openc3_token) tlm(*args[0..-2], type: args[-1].intern, scope: scope, token: token) end # Set a telemetry item in the current value table. # # Note: If this is done while OpenC3 is currently receiving telemetry, # this value could get overwritten at any time. Thus this capability is # best used for testing or for telemetry items that are not received # regularly through the target interface. # # Accepts two different calling styles: # set_tlm("TGT PKT ITEM = 1.0") # set_tlm('TGT','PKT','ITEM', 10.0) # # Favor the first syntax where possible as it is more succinct. # # @param args [String|Array] See the description for calling style # @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS def set_tlm(*args, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token) target_name, packet_name, item_name, value = set_tlm_process_args(args, __method__, scope: scope) authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope, token: token) CvtModel.set_item(target_name, packet_name, item_name, value, type: type.intern, scope: scope) end # Injects a packet into the system as if it was received from an interface # # @param target_name [String] Target name of the packet # @param packet_name [String] Packet name of the packet # @param item_hash [Hash] Hash of item_name and value for each item you want to change from the current value table # @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope, token: token) type = type.to_s.intern unless CvtModel::VALUE_TYPES.include?(type) raise "Unknown type '#{type}' for #{target_name} #{packet_name}" end if item_hash # Check that the items exist ... exceptions are raised if not TargetModel.packet_items(target_name, packet_name, item_hash.keys, scope: scope) else # Check that the packet exists ... exceptions are raised if not TargetModel.packet(target_name, packet_name, scope: scope) end packet_hash = get_telemetry(target_name, packet_name, scope: scope, token: token) packet = Packet.from_json(packet_hash) if item_hash item_hash.each do |name, value| packet.write(name.to_s, value, type) end end topic = "#{scope}__TELEMETRY__{#{target_name}}__#{packet_name}" msg_id, msg_hash = Topic.get_newest_message(topic) if msg_id packet.received_count = msg_hash['received_count'].to_i + 1 else packet.received_count = 1 end packet.received_time = Time.now.sys TelemetryTopic.write_packet(packet, scope: scope) end # Override the current value table such that a particular item always # returns the same value (for a given type) even when new telemetry # packets are received from the target. # # Accepts two different calling styles: # override_tlm("TGT PKT ITEM = 1.0") # override_tlm('TGT','PKT','ITEM', 10.0) # # Favor the first syntax where possible as it is more succinct. # # @param args The args must either be a string followed by a value or # three strings followed by a value (see the calling style in the # description). # @param type [Symbol] Telemetry type, :ALL (default), :RAW, :CONVERTED, :FORMATTED, :WITH_UNITS def override_tlm(*args, type: :ALL, scope: $openc3_scope, token: $openc3_token) target_name, packet_name, item_name, value = set_tlm_process_args(args, __method__, scope: scope) authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope, token: token) CvtModel.override(target_name, packet_name, item_name, value, type: type.intern, scope: scope) end # Normalize a telemetry item in a packet to its default behavior. Called # after override_tlm to restore standard processing. # # Accepts two different calling styles: # normalize_tlm("TGT PKT ITEM") # normalize_tlm('TGT','PKT','ITEM') # # Favor the first syntax where possible as it is more succinct. # # @param args The args must either be a string or three strings # (see the calling style in the description). # @param type [Symbol] Telemetry type, :ALL (default), :RAW, :CONVERTED, :FORMATTED, :WITH_UNITS # Also takes :ALL which means to normalize all telemetry types def normalize_tlm(*args, type: :ALL, scope: $openc3_scope, token: $openc3_token) target_name, packet_name, item_name = tlm_process_args(args, __method__, scope: scope) authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope, token: token) CvtModel.normalize(target_name, packet_name, item_name, type: type.intern, scope: scope) end # Returns the raw buffer for a telemetry packet. # # @param target_name [String] Name of the target # @param packet_name [String] Name of the packet # @return [Hash] telemetry hash with last telemetry buffer def get_tlm_buffer(target_name, packet_name, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) TargetModel.packet(target_name, packet_name, scope: scope) topic = "#{scope}__TELEMETRY__{#{target_name}}__#{packet_name}" msg_id, msg_hash = Topic.get_newest_message(topic) if msg_id msg_hash['buffer'] = msg_hash['buffer'].b return msg_hash end return nil end # Returns all the values (along with their limits state) for a packet. # # @param target_name [String] Name of the target # @param packet_name [String] Name of the packet # @param stale_time [Integer] Time in seconds from Time.now that packet will be marked stale # @param type [Symbol] Types returned, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS # @return [Array] Returns an Array consisting # of [item name, item value, item limits state] where the item limits # state can be one of {OpenC3::Limits::LIMITS_STATES} def get_tlm_packet(target_name, packet_name, stale_time: 30, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) packet = TargetModel.packet(target_name, packet_name, scope: scope) t = _validate_tlm_type(type) raise ArgumentError, "Unknown type '#{type}' for #{target_name} #{packet_name}" if t.nil? items = packet['items'].map { | item | item['name'] } cvt_items = items.map { | item | "#{target_name}__#{packet_name}__#{item}__#{type}" } current_values = CvtModel.get_tlm_values(cvt_items, stale_time: stale_time, scope: scope) items.zip(current_values).map { | item , values | [item, values[0], values[1]]} end # Returns all the item values (along with their limits state). The items # can be from any target and packet and thus must be fully qualified with # their target and packet names. # # @since 5.0.0 # @param items [Array] Array of items consisting of 'tgt__pkt__item__type' # @param stale_time [Integer] Time in seconds from Time.now that data will be marked stale # @return [Array] # Array consisting of the item value and limits state # given as symbols such as :RED, :YELLOW, :STALE def get_tlm_values(items, stale_time: 30, scope: $openc3_scope, token: $openc3_token) if !items.is_a?(Array) || !items[0].is_a?(String) raise ArgumentError, "items must be array of strings: ['TGT__PKT__ITEM__TYPE', ...]" end items.each_with_index do |item, index| target_name, packet_name, item_name, item_type = item.split('__') if packet_name == 'LATEST' _, packet_name, _ = tlm_process_args([target_name, packet_name, item_name], 'get_tlm_values', scope: scope) # Figure out which packet is LATEST items[index] = "#{target_name}__#{packet_name}__#{item_name}__#{item_type}" # Replace LATEST with the real packet name end authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) end CvtModel.get_tlm_values(items, stale_time: stale_time, scope: scope) end # Returns an array of all the telemetry packet hashes # # @since 5.0.0 # @param target_name [String] Name of the target # @return [Array] Array of all telemetry packet hashes def get_all_telemetry(target_name, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'tlm', target_name: target_name, scope: scope, token: token) TargetModel.packets(target_name, type: :TLM, scope: scope) end # Returns an array of all the telemetry packet names # # @since 5.0.6 # @param target_name [String] Name of the target # @return [Array] Array of all telemetry packet names def get_all_telemetry_names(target_name, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'cmd_info', target_name: target_name, scope: scope, token: token) TargetModel.packet_names(target_name, type: :TLM, scope: scope) end # Returns a telemetry packet hash # # @since 5.0.0 # @param target_name [String] Name of the target # @param packet_name [String] Name of the packet # @return [Hash] Telemetry packet hash def get_telemetry(target_name, packet_name, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) TargetModel.packet(target_name, packet_name, scope: scope) end # Returns a telemetry packet item hash # # @since 5.0.0 # @param target_name [String] Name of the target # @param packet_name [String] Name of the packet # @param item_name [String] Name of the packet # @return [Hash] Telemetry packet item hash def get_item(target_name, packet_name, item_name, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) TargetModel.packet_item(target_name, packet_name, item_name, scope: scope) end # 2x double underscore since __ is reserved SUBSCRIPTION_DELIMITER = '____' # Subscribe to a list of packets. An ID is returned which is passed to # get_packets(id) to return packets. # # @param packets [Array>] Array of arrays consisting of target name, packet name # @return [String] ID which should be passed to get_packets def subscribe_packets(packets, scope: $openc3_scope, token: $openc3_token) if !packets.is_a?(Array) || !packets[0].is_a?(Array) raise ArgumentError, "packets must be nested array: [['TGT','PKT'],...]" end result = {} packets.each do |target_name, packet_name| authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) topic = "#{scope}__DECOM__{#{target_name}}__#{packet_name}" id, _ = Topic.get_newest_message(topic) result[topic] = id ? id : '0-0' end result.to_a.join(SUBSCRIPTION_DELIMITER) end # Alias the singular as well since that matches COSMOS 4 alias subscribe_packet subscribe_packets # Get packets based on ID returned from subscribe_packet. # @param id [String] ID returned from subscribe_packets or last call to get_packets # @param block [Integer] Unused - Blocking must be implemented at the client # @param count [Integer] Maximum number of packets to return from EACH packet stream # @return [Array] Array of the ID and array of all packets found def get_packets(id, block: nil, count: 1000, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'tlm', scope: scope, token: token) # Split the list of topic, ID values and turn it into a hash for easy updates lookup = Hash[*id.split(SUBSCRIPTION_DELIMITER)] xread = Topic.read_topics(lookup.keys, lookup.values, nil, count) # Always don't block # Return the original ID and and empty array if we didn't get anything packets = [] return [id, packets] if xread.empty? xread.each do |topic, data| data.each do |id, msg_hash| lookup[topic] = id # save the new ID json_hash = JSON.parse(msg_hash['json_data'], :allow_nan => true, :create_additions => true) msg_hash.delete('json_data') packets << msg_hash.merge(json_hash) end end return lookup.to_a.join(SUBSCRIPTION_DELIMITER), packets end # Get the receive count for a telemetry packet # # @param target_name [String] Name of the target # @param packet_name [String] Name of the packet # @return [Numeric] Receive count for the telemetry packet def get_tlm_cnt(target_name, packet_name, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'system', target_name: target_name, packet_name: packet_name, scope: scope, token: token) TargetModel.packet(target_name, packet_name, scope: scope) Topic.get_cnt("#{scope}__TELEMETRY__{#{target_name}}__#{packet_name}") end # Get the transmit counts for telemetry packets # # @param target_packets [Array>] Array of arrays containing target_name, packet_name # @return [Numeric] Transmit count for the command def get_tlm_cnts(target_packets, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'system', scope: scope, token: token) counts = [] target_packets.each do |target_name, packet_name| counts << Topic.get_cnt("#{scope}__TELEMETRY__{#{target_name}}__#{packet_name}") end counts end # Get the list of derived telemetry items for a packet # # @param target_name [String] Target name # @param packet_name [String] Packet name # @return [Array] All of the ignored telemetry items for a packet. def get_packet_derived_items(target_name, packet_name, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) packet = TargetModel.packet(target_name, packet_name, scope: scope) return packet['items'].select { |item| item['data_type'] == 'DERIVED' }.map { |item| item['name'] } end # PRIVATE def _validate_tlm_type(type) case type.intern when :RAW return '' when :CONVERTED return 'C' when :FORMATTED return 'F' when :WITH_UNITS return 'U' end return nil end def tlm_process_args(args, function_name, scope: $openc3_scope, token: $openc3_token) case args.length when 1 target_name, packet_name, item_name = extract_fields_from_tlm_text(args[0]) when 3 target_name = args[0] packet_name = args[1] item_name = args[2] else # Invalid number of arguments raise "ERROR: Invalid number of arguments (#{args.length}) passed to #{function_name}()" end if packet_name == 'LATEST' latest = -1 TargetModel.packets(target_name, scope: scope).each do |packet| item = packet['items'].find { |item| item['name'] == item_name } if item # TODO: Fixme: This should be using the CVT not topics - Will possibly choose wrong packet if mixed with stored _, msg_hash = Topic.get_newest_message("#{scope}__DECOM__{#{target_name}}__#{packet['packet_name']}") packet_name = packet['packet_name'] if packet_name == 'LATEST' if msg_hash && msg_hash['time'] && msg_hash['time'].to_i > latest packet_name = packet['packet_name'] latest = msg_hash['time'].to_i end end end raise "Item '#{target_name} LATEST #{item_name}' does not exist" if latest == -1 else # Determine if this item exists, it will raise appropriate errors if not TargetModel.packet_item(target_name, packet_name, item_name, scope: scope) end return [target_name, packet_name, item_name] end def set_tlm_process_args(args, function_name, scope: $openc3_scope, token: $openc3_token) case args.length when 1 target_name, packet_name, item_name, value = extract_fields_from_set_tlm_text(args[0]) when 4 target_name = args[0] packet_name = args[1] item_name = args[2] value = args[3] else # Invalid number of arguments raise "ERROR: Invalid number of arguments (#{args.length}) passed to #{function_name}()" end # Determine if this item exists, it will raise appropriate errors if not TargetModel.packet_item(target_name, packet_name, item_name, scope: scope) return [target_name, packet_name, item_name, value] end end end