From d83f1cd0d7ed705cd86e80124a9b0b1fdee40578 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Thu, 12 Oct 2023 12:19:32 -0600 Subject: [PATCH 01/17] Full tlm_api testing --- .../scripts/running_script.py | 4 +- openc3/lib/openc3/api/tlm_api.rb | 92 +- openc3/python/openc3/api/tlm_api.py | 123 ++- .../microservices/interface_decom_common.py | 2 +- .../microservices/interface_microservice.py | 4 +- openc3/python/openc3/models/cvt_model.py | 93 +- openc3/python/openc3/models/target_model.py | 52 + openc3/python/openc3/packets/packet.py | 29 +- .../openc3/topics/telemetry_decom_topic.py | 4 +- openc3/python/openc3/utilities/extract.py | 19 +- openc3/python/test/api/test_tlm_api.py | 983 +++++++++++++++++- .../config/targets/INST/cmd_tlm/inst_tlm.txt | 15 + .../test_interface_microservice.py | 4 +- openc3/python/test/models/test_cvt_model.py | 4 +- openc3/python/test/test_helper.py | 13 + 15 files changed, 1242 insertions(+), 199 deletions(-) diff --git a/openc3-cosmos-script-runner-api/scripts/running_script.py b/openc3-cosmos-script-runner-api/scripts/running_script.py index 3de1646bde..6922bebafc 100644 --- a/openc3-cosmos-script-runner-api/scripts/running_script.py +++ b/openc3-cosmos-script-runner-api/scripts/running_script.py @@ -498,7 +498,6 @@ def pre_line_instrumentation( self.handle_potential_tab_change(filename) - line_number = line_number + self.line_offset detail_string = None if filename: @@ -1275,9 +1274,8 @@ def start(procedure_name): setattr(openc3.script, "start", start) -# Require an additional ruby file +# Load an additional python file def load_utility(procedure_name): - # Ensure require_utility works like require where you don't need the .rb extension extension = os.path.splitext(procedure_name)[1] if extension != ".py": procedure_name += ".py" diff --git a/openc3/lib/openc3/api/tlm_api.rb b/openc3/lib/openc3/api/tlm_api.rb index 6dd564532a..7b752fa55c 100644 --- a/openc3/lib/openc3/api/tlm_api.rb +++ b/openc3/lib/openc3/api/tlm_api.rb @@ -66,27 +66,27 @@ module Api # @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, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + def tlm(*args, type: :CONVERTED, cache_timeout: 0.1, scope: $openc3_scope) target_name, packet_name, item_name = tlm_process_args(args, 'tlm', cache_timeout: cache_timeout, scope: scope) - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) CvtModel.get_item(target_name, packet_name, item_name, type: type.intern, cache_timeout: cache_timeout, scope: scope) end - def tlm_raw(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) - tlm(*args, type: :RAW, cache_timeout: cache_timeout, scope: scope, token: token) + def tlm_raw(*args, cache_timeout: 0.1, scope: $openc3_scope) + tlm(*args, type: :RAW, cache_timeout: cache_timeout, scope: scope) end - def tlm_formatted(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) - tlm(*args, type: :FORMATTED, cache_timeout: cache_timeout, scope: scope, token: token) + def tlm_formatted(*args, cache_timeout: 0.1, scope: $openc3_scope) + tlm(*args, type: :FORMATTED, cache_timeout: cache_timeout, scope: scope) end - def tlm_with_units(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) - tlm(*args, type: :WITH_UNITS, cache_timeout: cache_timeout, scope: scope, token: token) + def tlm_with_units(*args, cache_timeout: 0.1, scope: $openc3_scope) + tlm(*args, type: :WITH_UNITS, cache_timeout: cache_timeout, scope: scope) end # @deprecated Use tlm with type: - def tlm_variable(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) - tlm(*args[0..-2], type: args[-1].intern, cache_timeout: cache_timeout, scope: scope, token: token) + def tlm_variable(*args, cache_timeout: 0.1, scope: $openc3_scope) + tlm(*args[0..-2], type: args[-1].intern, cache_timeout: cache_timeout, scope: scope) end # Set a telemetry item in the current value table. @@ -104,9 +104,9 @@ def tlm_variable(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3 # # @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) + def set_tlm(*args, type: :CONVERTED, scope: $openc3_scope) 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) + authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope) CvtModel.set_item(target_name, packet_name, item_name, value, type: type.intern, scope: scope) end @@ -116,8 +116,8 @@ def set_tlm(*args, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token) # @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) + def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, scope: $openc3_scope) + authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope) type = type.to_s.intern target_name = target_name.upcase packet_name = packet_name.upcase @@ -165,15 +165,15 @@ def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, scop # 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) + def override_tlm(*args, type: :ALL, scope: $openc3_scope) 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) + authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope) CvtModel.override(target_name, packet_name, item_name, value, type: type.intern, scope: scope) end # Get the list of CVT overrides - def get_overrides(scope: $openc3_scope, token: $openc3_token) - authorize(permission: 'tlm', scope: scope, token: token) + def get_overrides(scope: $openc3_scope) + authorize(permission: 'tlm', scope: scope) CvtModel.overrides(scope: scope) end @@ -190,9 +190,9 @@ def get_overrides(scope: $openc3_scope, token: $openc3_token) # (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) + def normalize_tlm(*args, type: :ALL, scope: $openc3_scope) 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) + authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope) CvtModel.normalize(target_name, packet_name, item_name, type: type.intern, scope: scope) end @@ -201,10 +201,10 @@ def normalize_tlm(*args, type: :ALL, scope: $openc3_scope, token: $openc3_token) # @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) + def get_tlm_buffer(target_name, packet_name, scope: $openc3_scope) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) TargetModel.packet(target_name, packet_name, scope: scope) topic = "#{scope}__TELEMETRY__{#{target_name}}__#{packet_name}" msg_id, msg_hash = Topic.get_newest_message(topic) @@ -224,10 +224,10 @@ def get_tlm_buffer(target_name, packet_name, scope: $openc3_scope, token: $openc # @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, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + def get_tlm_packet(target_name, packet_name, stale_time: 30, type: :CONVERTED, cache_timeout: 0.1, scope: $openc3_scope) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) 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? @@ -247,7 +247,7 @@ def get_tlm_packet(target_name, packet_name, stale_time: 30, type: :CONVERTED, c # @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, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + def get_tlm_values(items, stale_time: 30, cache_timeout: 0.1, scope: $openc3_scope) if !items.is_a?(Array) || !items[0].is_a?(String) raise ArgumentError, "items must be array of strings: ['TGT__PKT__ITEM__TYPE', ...]" end @@ -264,7 +264,7 @@ def get_tlm_values(items, stale_time: 30, cache_timeout: 0.1, scope: $openc3_sco end packets.uniq! packets.each do |target_name, packet_name| - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) end CvtModel.get_tlm_values(cvt_items, stale_time: stale_time, cache_timeout: cache_timeout, scope: scope) end @@ -274,9 +274,9 @@ def get_tlm_values(items, stale_time: 30, cache_timeout: 0.1, scope: $openc3_sco # @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) + def get_all_telemetry(target_name, scope: $openc3_scope) target_name = target_name.upcase - authorize(permission: 'tlm', target_name: target_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, scope: scope) TargetModel.packets(target_name, type: :TLM, scope: scope) end @@ -285,9 +285,9 @@ def get_all_telemetry(target_name, scope: $openc3_scope, token: $openc3_token) # @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) + def get_all_telemetry_names(target_name, scope: $openc3_scope) target_name = target_name.upcase - authorize(permission: 'cmd_info', target_name: target_name, scope: scope, token: token) + authorize(permission: 'cmd_info', target_name: target_name, scope: scope) TargetModel.packet_names(target_name, type: :TLM, scope: scope) end @@ -297,10 +297,10 @@ def get_all_telemetry_names(target_name, scope: $openc3_scope, token: $openc3_to # @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) + def get_telemetry(target_name, packet_name, scope: $openc3_scope) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) TargetModel.packet(target_name, packet_name, scope: scope) end @@ -311,11 +311,11 @@ def get_telemetry(target_name, packet_name, scope: $openc3_scope, token: $openc3 # @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) + def get_item(target_name, packet_name, item_name, scope: $openc3_scope) target_name = target_name.upcase packet_name = packet_name.upcase item_name = item_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) TargetModel.packet_item(target_name, packet_name, item_name, scope: scope) end @@ -327,7 +327,7 @@ def get_item(target_name, packet_name, item_name, scope: $openc3_scope, token: $ # # @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) + def subscribe_packets(packets, scope: $openc3_scope) if !packets.is_a?(Array) || !packets[0].is_a?(Array) raise ArgumentError, "packets must be nested array: [['TGT','PKT'],...]" end @@ -336,7 +336,7 @@ def subscribe_packets(packets, scope: $openc3_scope, token: $openc3_token) packets.each do |target_name, packet_name| target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) topic = "#{scope}__DECOM__{#{target_name}}__#{packet_name}" id, _ = Topic.get_newest_message(topic) result[topic] = id ? id : '0-0' @@ -351,8 +351,8 @@ def subscribe_packets(packets, scope: $openc3_scope, token: $openc3_token) # @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) + def get_packets(id, block: nil, count: 1000, scope: $openc3_scope) + authorize(permission: 'tlm', scope: scope) # 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 @@ -375,10 +375,10 @@ def get_packets(id, block: nil, count: 1000, scope: $openc3_scope, token: $openc # @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) + def get_tlm_cnt(target_name, packet_name, scope: $openc3_scope) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'system', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'system', target_name: target_name, packet_name: packet_name, scope: scope) TargetModel.packet(target_name, packet_name, scope: scope) Topic.get_cnt("#{scope}__TELEMETRY__{#{target_name}}__#{packet_name}") end @@ -387,8 +387,8 @@ def get_tlm_cnt(target_name, packet_name, scope: $openc3_scope, token: $openc3_t # # @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) + def get_tlm_cnts(target_packets, scope: $openc3_scope) + authorize(permission: 'system', scope: scope) counts = [] target_packets.each do |target_name, packet_name| target_name = target_name.upcase @@ -403,10 +403,10 @@ def get_tlm_cnts(target_packets, scope: $openc3_scope, token: $openc3_token) # @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) + def get_packet_derived_items(target_name, packet_name, scope: $openc3_scope) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) packet = TargetModel.packet(target_name, packet_name, scope: scope) return packet['items'].select { |item| item['data_type'] == 'DERIVED' }.map { |item| item['name'] } end @@ -427,7 +427,7 @@ def _validate_tlm_type(type) return nil end - def tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + def tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope) case args.length when 1 target_name, packet_name, item_name = extract_fields_from_tlm_text(args[0]) @@ -453,7 +453,7 @@ def tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope return [target_name, packet_name, item_name] end - def set_tlm_process_args(args, method_name, scope: $openc3_scope, token: $openc3_token) + def set_tlm_process_args(args, method_name, scope: $openc3_scope) case args.length when 1 target_name, packet_name, item_name, value = extract_fields_from_set_tlm_text(args[0]) diff --git a/openc3/python/openc3/api/tlm_api.py b/openc3/python/openc3/api/tlm_api.py index 30289c0d03..7ebaba03e3 100644 --- a/openc3/python/openc3/api/tlm_api.py +++ b/openc3/python/openc3/api/tlm_api.py @@ -30,6 +30,9 @@ WHITELIST.extend( [ "tlm", + "tlm_raw", + "tlm_formatted", + "tlm_with_units", "set_tlm", "inject_tlm", "override_tlm", @@ -62,12 +65,28 @@ # @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): - target_name, packet_name, item_name = _tlm_process_args(args, "tlm", scope=scope) +def tlm(*args, type="CONVERTED", cache_timeout=0.1, scope=OPENC3_SCOPE): + target_name, packet_name, item_name = _tlm_process_args( + args, "tlm", cache_timeout=cache_timeout, scope=scope + ) authorize( permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope ) - return CvtModel.get_item(target_name, packet_name, item_name, type, scope) + return CvtModel.get_item( + target_name, packet_name, item_name, type, cache_timeout, scope + ) + + +def tlm_raw(*args, cache_timeout=0.1, scope=OPENC3_SCOPE): + return tlm(*args, type="RAW", cache_timeout=cache_timeout, scope=scope) + + +def tlm_formatted(*args, cache_timeout=0.1, scope=OPENC3_SCOPE): + return tlm(*args, type="FORMATTED", cache_timeout=cache_timeout, scope=scope) + + +def tlm_with_units(*args, cache_timeout=0.1, scope=OPENC3_SCOPE): + return tlm(*args, type="WITH_UNITS", cache_timeout=cache_timeout, scope=scope) # Set a telemetry item in the current value table. @@ -87,7 +106,7 @@ def tlm(*args, type="CONVERTED", scope=OPENC3_SCOPE): # @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED, or :WITH_UNITS def set_tlm(*args, type="CONVERTED", scope=OPENC3_SCOPE): target_name, packet_name, item_name, value = _set_tlm_process_args( - args, "set_tlm", scope=scope + args, "set_tlm", scope ) authorize( permission="tlm_set", @@ -95,9 +114,7 @@ def set_tlm(*args, type="CONVERTED", scope=OPENC3_SCOPE): packet_name=packet_name, scope=scope, ) - CvtModel.set_item( - target_name, packet_name, item_name, value, type=type, scope=scope - ) + CvtModel.set_item(target_name, packet_name, item_name, value, type, scope) # Injects a packet into the system as if it was received from an interface @@ -123,7 +140,9 @@ def inject_tlm( if item_hash: item_hash = {k.upper(): v for k, v in item_hash.items()} # Check that the items exist ... exceptions are raised if not - TargetModel.packet_items(target_name, packet_name, item_hash.keys, scope=scope) + 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) @@ -162,7 +181,7 @@ def inject_tlm( # @param type [Symbol] Telemetry type, :ALL (default), :RAW, :CONVERTED, :FORMATTED, :WITH_UNITS def override_tlm(*args, type="ALL", scope=OPENC3_SCOPE): target_name, packet_name, item_name, value = _set_tlm_process_args( - args, "override_tlm", scope=scope + args, "override_tlm", scope ) authorize( permission="tlm_set", @@ -178,7 +197,7 @@ def override_tlm(*args, type="ALL", scope=OPENC3_SCOPE): # Get the list of CVT overrides def get_overrides(scope=OPENC3_SCOPE): authorize(permission="tlm", scope=scope) - CvtModel.overrides(scope=scope) + return CvtModel.overrides(scope=scope) # Normalize a telemetry item in a packet to its default behavior. Called @@ -222,9 +241,8 @@ def get_tlm_buffer(target_name, packet_name, scope=OPENC3_SCOPE): topic = f"{scope}__TELEMETRY__{{{target_name}}}__{packet_name}" msg_id, msg_hash = Topic.get_newest_message(topic) if msg_id: - # TODO: Python equivalent of .b - # msg_hash['buffer'] = msg_hash['buffer'].b - return msg_hash + # Decode the keys for user convenience + return {k.decode(): v for (k, v) in msg_hash.items()} return None @@ -238,7 +256,7 @@ def get_tlm_buffer(target_name, packet_name, scope=OPENC3_SCOPE): # 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( - self, target_name, packet_name, stale_time=30, type="CONVERTED", scope=OPENC3_SCOPE + target_name, packet_name, stale_time=30, type="CONVERTED", scope=OPENC3_SCOPE ): target_name = target_name.upper() packet_name = packet_name.upper() @@ -247,14 +265,22 @@ def get_tlm_packet( ) packet = TargetModel.packet(target_name, packet_name, scope=scope) t = _validate_tlm_type(type) - if not t: + if t is None: raise AttributeError(f"Unknown type '{type}' for {target_name} {packet_name}") - items = {item["name"].upper() for item in packet["items"]} - cvt_items = {f"{target_name}__{packet_name}__{item}__{type}" for item in items} + cvt_items = [ + [target_name, packet_name, item["name"].upper(), type] + for item in packet["items"] + ] + # This returns an array of arrays containin the value and the limits state: + # [[0, None], [0, 'RED_LOW'], ... ] current_values = CvtModel.get_tlm_values( cvt_items, stale_time=stale_time, scope=scope ) - return {[item, values[0], values[1]] for item, values in current_values} + result = [] + # Combine the values with the item name + for index, item in enumerate(current_values): + result.append([cvt_items[index][2], item[0], item[1]]) + return result # Returns all the item values (along with their limits state). The items @@ -266,32 +292,35 @@ def get_tlm_packet( # @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): - if type(items) != list or type(items[0]) != str: +def get_tlm_values(items, stale_time=30, cache_timeout=0.1, scope=OPENC3_SCOPE): + if type(items) != list or len(items) == 0 or type(items[0]) != str: raise AttributeError( "items must be array of strings: ['TGT__PKT__ITEM__TYPE', ...]" ) - for index, item in enumerate(items): - target_name, packet_name, item_name, value_type = item.split("__") - if not target_name or not packet_name or not item_name or not value_type: + packets = [] + cvt_items = [] + for item in items: + try: + target_name, packet_name, item_name, value_type = item.upper().split("__") + except ValueError: raise AttributeError("items must be formatted as TGT__PKT__ITEM__TYPE") - target_name = target_name.upper() - packet_name = packet_name.upper() - item_name = item_name.upper() - value_type = value_type.upper() 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 + packet_name = CvtModel.determine_latest_packet_for_item( + target_name, item_name, cache_timeout, scope + ) # Change packet_name in case of LATEST and ensure upcase - items[index] = f"{target_name}__{packet_name}__{item_name}__{value_type}" + cvt_items.append([target_name, packet_name, item_name, value_type]) + packets.append([target_name, packet_name]) + # Make the array of arrays unique + packets = [list(x) for x in set(tuple(x) for x in packets)] + for name in packets: authorize( permission="tlm", - target_name=target_name, - packet_name=packet_name, + target_name=name[0], + packet_name=name[1], scope=scope, ) - return CvtModel.get_tlm_values(items, stale_time=stale_time, scope=scope) + return CvtModel.get_tlm_values(cvt_items, stale_time, cache_timeout, scope) # Returns an array of all the telemetry packet hashes @@ -424,7 +453,9 @@ def get_tlm_cnts(target_packets, scope=OPENC3_SCOPE): for target_name, packet_name in target_packets: target_name = target_name.upper() packet_name = packet_name.upper() - counts << Topic.get_cnt(f"{scope}__TELEMETRY__{{{target_name}}}__{packet_name}") + counts.append( + Topic.get_cnt(f"{scope}__TELEMETRY__{{{target_name}}}__{packet_name}") + ) return counts @@ -456,7 +487,7 @@ def _validate_tlm_type(type): return None -def _tlm_process_args(args, method_name, scope=OPENC3_SCOPE): +def _tlm_process_args(args, method_name, cache_timeout=0.1, scope=OPENC3_SCOPE): match (len(args)): case 1: target_name, packet_name, item_name = extract_fields_from_tlm_text(args[0]) @@ -473,24 +504,10 @@ def _tlm_process_args(args, method_name, scope=OPENC3_SCOPE): packet_name = packet_name.upper() item_name = item_name.upper() if packet_name == "LATEST": - latest = -1 - for packet in TargetModel.packets(target_name, scope=scope): - found = None - for item in packet["items"]: - if item["name"] == item_name: - found = item - break - if found: - hash = CvtModel.get(target_name, packet["packet_name"], scope) - if hash["PACKET_TIMESECONDS"] and hash["PACKET_TIMESECONDS"] > latest: - latest = hash["PACKET_TIMESECONDS"] - packet_name = packet["packet_name"] - if latest == -1: - raise RuntimeError( - f"Item '{target_name} LATEST {item_name}' does not exist" - ) + packet_name = CvtModel.determine_latest_packet_for_item( + target_name, item_name, cache_timeout, scope + ) else: - pass # 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 diff --git a/openc3/python/openc3/microservices/interface_decom_common.py b/openc3/python/openc3/microservices/interface_decom_common.py index 9da82b132b..35f8bbc0ff 100644 --- a/openc3/python/openc3/microservices/interface_decom_common.py +++ b/openc3/python/openc3/microservices/interface_decom_common.py @@ -31,7 +31,7 @@ def handle_inject_tlm(inject_tlm_json, scope): type = str(inject_tlm_hash["type"]) packet = System.telemetry.packet(target_name, packet_name) if item_hash: - for name, value in item_hash: + for name, value in item_hash.items(): packet.write(str(name), value, type) packet.received_count += 1 packet.received_time = datetime.now(timezone.utc) diff --git a/openc3/python/openc3/microservices/interface_microservice.py b/openc3/python/openc3/microservices/interface_microservice.py index 913b7dd6b1..25319642e3 100644 --- a/openc3/python/openc3/microservices/interface_microservice.py +++ b/openc3/python/openc3/microservices/interface_microservice.py @@ -635,8 +635,8 @@ def handle_packet(self, packet): json_hash = CvtModel.build_json_from_packet(packet) CvtModel.set( json_hash, - target_name=packet.target_name, - packet_name=packet.packet_name, + packet.target_name, + packet.packet_name, scope=self.scope, ) num_bytes_to_print = min( diff --git a/openc3/python/openc3/models/cvt_model.py b/openc3/python/openc3/models/cvt_model.py index b547df60ed..ebe1f1ba76 100644 --- a/openc3/python/openc3/models/cvt_model.py +++ b/openc3/python/openc3/models/cvt_model.py @@ -20,7 +20,7 @@ from openc3.models.model import Model from openc3.models.target_model import TargetModel from openc3.environment import OPENC3_SCOPE -from openc3.utilities.json import JsonEncoder +from openc3.utilities.json import JsonEncoder, JsonDecoder class CvtModel(Model): @@ -64,7 +64,7 @@ def get(cls, target_name, packet_name, cache_timeout=0.1, scope=OPENC3_SCOPE): packet = Store.hget(key, packet_name) if packet is None: raise RuntimeError(f"Packet '{target_name} {packet_name}' does not exist") - hash = json.loads(packet) + hash = json.loads(packet, cls=JsonDecoder) CvtModel.packet_cache[tgt_pkt_key] = [now, hash] return hash @@ -74,8 +74,8 @@ def set_item( cls, target_name, packet_name, item_name, value, type, scope=OPENC3_SCOPE ): hash = cls.get( - target_name=target_name, - packet_name=packet_name, + target_name, + packet_name, cache_timeout=0.0, scope=scope, ) @@ -128,7 +128,7 @@ def get_item( ) if result is not None: return result - hash = cls.get(target_name=target_name, packet_name=packet_name, scope=scope) + hash = cls.get(target_name, packet_name, scope=scope) for result in [hash[x] for x in types if x in hash]: if result is not None: if type == "FORMATTED" or type == "WITH_UNITS": @@ -159,10 +159,10 @@ def get_tlm_values( for target_packet_key, target_name, packet_name, value_keys in lookups: if target_packet_key not in packet_lookup: packet_lookup[target_packet_key] = cls.get( - target_name=target_name, - packet_name=packet_name, - cache_timeout=cache_timeout, - scope=scope, + target_name, + packet_name, + cache_timeout, + scope, ) hash = packet_lookup[target_packet_key] item_result = [] @@ -174,22 +174,20 @@ def get_tlm_values( item_result.insert(0, hash[key]) break # We want the first value # If we were able to find a value, try to get the limits state - if len(item_result) > 0: - print( - f"\nnow:{now} rxtime:{hash['RECEIVED_TIMESECONDS']} diff:{now - hash['RECEIVED_TIMESECONDS']} stale:{stale_time}\n" - ) + if len(item_result) > 0 and item_result[0] is not None: if now - hash["RECEIVED_TIMESECONDS"] > stale_time: - print(f"{target_name} {packet_name} {value_keys[-1]} STALE!!!") item_result.insert(1, "STALE") else: # The last key is simply the name (RAW) so we can append __L # If there is no limits then it returns None which is acceptable item_result.insert(1, hash.get(f"{value_keys[-1]}__L")) else: - if hash.get(value_keys[-1]) is None: + if value_keys[-1] not in hash: raise RuntimeError( f"Item '{target_name} {packet_name} {value_keys[-1]}' does not exist" ) + else: + item_result.insert(1, None) results.append(item_result) return results @@ -204,7 +202,7 @@ def overrides(cls, scope=OPENC3_SCOPE): # decode the binary string keys to strings all = {k.decode(): v for (k, v) in all.items()} for packet_name, hash in all.items(): - items = json.loads(hash) + items = json.loads(hash, cls=JsonDecoder) for key, value in items.items(): item = {} item["target_name"] = target_name @@ -270,30 +268,32 @@ def normalize( hash = json.loads(hash) else: hash = {} - try: - match type: - case "ALL": + match type: + case "ALL": + if item_name in hash: hash.pop(item_name) + if f"{item_name}__C" in hash: hash.pop(f"{item_name}__C") + if f"{item_name}__F" in hash: hash.pop(f"{item_name}__F") + if f"{item_name}__U" in hash: hash.pop(f"{item_name}__U") - case "RAW": + case "RAW": + if item_name in hash: hash.pop(item_name) - case "CONVERTED": + case "CONVERTED": + if f"{item_name}__C" in hash: hash.pop(f"{item_name}__C") - case "FORMATTED": + case "FORMATTED": + if f"{item_name}__F" in hash: hash.pop(f"{item_name}__F") - case "WITH_UNITS": + case "WITH_UNITS": + if f"{item_name}__U" in hash: hash.pop(f"{item_name}__U") - case _: - raise RuntimeError( - f"Unknown type '{type}' for {target_name} {packet_name} {item_name}" - ) - # If any of the hash.pop lines fail we get a KeyError - # in this case don't set the override_cache or redis - # because it's probably just an item that hasn't been overriden - except KeyError: - return + case _: + raise RuntimeError( + f"Unknown type '{type}' for {target_name} {packet_name} {item_name}" + ) tgt_pkt_key = f"{scope}__tlm__{target_name}__{packet_name}" CvtModel.override_cache[tgt_pkt_key] = [time.time(), hash] if len(hash) == 0: @@ -303,6 +303,35 @@ def normalize( f"{scope}__override__{target_name}", packet_name, json.dumps(hash) ) + @classmethod + def determine_latest_packet_for_item( + cls, target_name, item_name, cache_timeout=0.1, scope=OPENC3_SCOPE + ): + item_map = TargetModel.get_item_to_packet_map(target_name, scope=scope) + packet_names = item_map.get(item_name) + if packet_names is None: + raise RuntimeError( + f"Item '{target_name} LATEST {item_name}' does not exist for scope: {scope}" + ) + + latest = -1 + latest_packet_name = None + for packet_name in packet_names: + hash = cls.get( + target_name, + packet_name, + cache_timeout, + scope, + ) + if hash["PACKET_TIMESECONDS"] and hash["PACKET_TIMESECONDS"] > latest: + latest = hash["PACKET_TIMESECONDS"] + latest_packet_name = packet_name + if latest == -1: + raise RuntimeError( + f"Item '{target_name} LATEST {item_name}' does not exist for scope: {scope}" + ) + return latest_packet_name + @classmethod def _handle_item_override( cls, diff --git a/openc3/python/openc3/models/target_model.py b/openc3/python/openc3/models/target_model.py index 805a6fe9b4..eeffcfd2ce 100644 --- a/openc3/python/openc3/models/target_model.py +++ b/openc3/python/openc3/models/target_model.py @@ -15,6 +15,7 @@ # if purchased from OpenC3, Inc. import json +import time from openc3.models.model import Model from openc3.utilities.store import Store from openc3.environment import OPENC3_SCOPE @@ -30,6 +31,8 @@ class TargetModel(Model): PRIMARY_KEY = "openc3_targets" VALID_TYPES = ["CMD", "TLM"] + ITEM_MAP_CACHE_TIMEOUT = 10.0 + item_map_cache = {} # NOTE: The following three class methods are used by the ModelController # and are reimplemented to enable various Model class methods to work @@ -101,6 +104,55 @@ def packet_item( ) return found + # @return [Array] Item hash array or raises an exception + @classmethod + def packet_items( + cls, target_name, packet_name, items, type="TLM", scope=OPENC3_SCOPE + ): + packet = cls.packet(target_name, packet_name, type=type, scope=scope) + found = [] + for item in packet["items"]: + if item["name"] in items: + found.append(item) + # found = packet['items'].find_all { |item| items.map(&:to_s).include?(item['name']) } + if len(found) != len(items): # we didn't find them all + found_items = [item["name"] for item in found] + not_found = [] + for item in items - found_items: + not_found.append(f"'{target_name} {packet_name} {item}'") + # 'does not exist' not gramatically correct but we use it in every other exception + raise RuntimeError(f"Item(s) {', '.join(not_found)} does not exist") + return found + + @classmethod + def get_item_to_packet_map(cls, target_name, scope=OPENC3_SCOPE): + if target_name in TargetModel.item_map_cache: + cache_time, item_map = TargetModel.item_map_cache[target_name] + if (time.time() - cache_time) < TargetModel.ITEM_MAP_CACHE_TIMEOUT: + return item_map + item_map_key = f"{scope}__{target_name}__item_to_packet_map" + target_name = target_name.upper() + json_data = Store.get(item_map_key) + if json_data: + item_map = json.loads(json_data) + else: + item_map = cls.build_item_to_packet_map(target_name, scope=scope) + Store.set(item_map_key, json.dumps(item_map)) + TargetModel.item_map_cache[target_name] = [time.time(), item_map] + return item_map + + @classmethod + def build_item_to_packet_map(cls, target_name, scope=OPENC3_SCOPE): + item_map = {} + for packet in cls.packets(target_name, scope=scope): + items = packet["items"] + for item in items: + item_name = item["name"] + if item_map.get(item_name) is None: + item_map[item_name] = [] + item_map[item_name].append(packet["packet_name"]) + return item_map + # TODO: Not nearly complete ... see target_model.rb def __init__( self, name, folder_name=None, updated_at=None, plugin=None, scope=OPENC3_SCOPE diff --git a/openc3/python/openc3/packets/packet.py b/openc3/python/openc3/packets/packet.py index feca00fbea..d8ff028c39 100644 --- a/openc3/python/openc3/packets/packet.py +++ b/openc3/python/openc3/packets/packet.py @@ -82,7 +82,7 @@ def __init__( self.limits_items_hash = {} self.processors = {} self.limits_change_callback = None - self.read_conversion_cache = None + self.read_conversion_cache = {} # self.short_buffer_allowed = None self.raw = None self.messages_disabled = False @@ -175,8 +175,8 @@ def received_time(self, received_time): raise AttributeError( f"received_time must be a datetime but is a {received_time.__class__.__name__}" ) - self.__received_time = received_time + self.read_conversion_cache = {} else: self.__received_time = None @@ -193,6 +193,7 @@ def received_count(self, received_count): ) self.__received_count = received_count + self.read_conversion_cache = {} # Tries to identify if a buffer represents the currently defined packet. It: # does this by iterating over all the packet items that were created with @@ -269,8 +270,7 @@ def buffer(self, buffer): Logger.error( f"{self.target_name} {self.packet_name} buffer ({type(buffer)}) received with actual packet length of {len(buffer)} but defined length of {self.defined_length}" ) - if self.read_conversion_cache: - self.read_conversion_cache = {} + self.read_conversion_cache = {} self.process() # Sets the received time of the packet (without cloning) @@ -278,9 +278,8 @@ def buffer(self, buffer): # self.param received_time [Time] Time this packet was received def set_received_time_fast(self, received_time): self.__received_time = received_time - if self.read_conversion_cache: - with self.synchronize(): - self.read_conversion_cache = {} + with self.synchronize(): + self.read_conversion_cache = {} @property def hazardous_description(self): @@ -540,7 +539,7 @@ def read_item(self, item, value_type="CONVERTED", buffer=None, given_raw=None): if item.read_conversion: using_cached_value = False check_cache = buffer == self.buffer - if check_cache and self.read_conversion_cache is not None: + if check_cache: with self.synchronize_allow_reads(): if self.read_conversion_cache.get(item.name): value = self.read_conversion_cache[item.name] @@ -562,8 +561,6 @@ def read_item(self, item, value_type="CONVERTED", buffer=None, given_raw=None): if check_cache: with self.synchronize_allow_reads(): - if self.read_conversion_cache is None: - self.read_conversion_cache = {} self.read_conversion_cache[item.name] = value # Make sure cached value is not modified by anyone by creating a deep copy @@ -699,9 +696,8 @@ def write_item(self, item, value, value_type="CONVERTED", buffer=None): raise AttributeError( f"Unknown value type '{value_type}', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'" ) - if self.read_conversion_cache: - with self.synchronize(): - self.read_conversion_cache = {} + with self.synchronize(): + self.read_conversion_cache = {} # Write values to the buffer based on the item definitions # @@ -953,9 +949,8 @@ def reset(self): self.received_count = 0 self.stored = False self.extra = None - if self.read_conversion_cache is not None: - with self.synchronize(): - self.read_conversion_cache = {} + with self.synchronize(): + self.read_conversion_cache = {} if not self.processors: return @@ -971,7 +966,7 @@ def clone(self): packet = super().clone() if self.processors is not None: packet.processors = copy.deepcopy(packet.processors) - packet.read_conversion_cache = None + packet.read_conversion_cache = {} if packet.extra is not None: packet.extra = copy.deepcopy(packet.extra) return packet diff --git a/openc3/python/openc3/topics/telemetry_decom_topic.py b/openc3/python/openc3/topics/telemetry_decom_topic.py index aa8a716e60..156158554a 100644 --- a/openc3/python/openc3/topics/telemetry_decom_topic.py +++ b/openc3/python/openc3/topics/telemetry_decom_topic.py @@ -52,7 +52,7 @@ def write_packet(cls, packet, id=None, scope=None): # Also update the current value table with the latest decommutated data CvtModel.set( json_hash, - target_name=packet.target_name, - packet_name=packet.packet_name, + packet.target_name, + packet.packet_name, scope=scope, ) diff --git a/openc3/python/openc3/utilities/extract.py b/openc3/python/openc3/utilities/extract.py index c87f5fcfa6..defd7e64b9 100644 --- a/openc3/python/openc3/utilities/extract.py +++ b/openc3/python/openc3/utilities/extract.py @@ -216,18 +216,17 @@ def extract_fields_from_set_tlm_text(text): # set_tlm("TGT PKT ITEM = 'new item'") # set_tlm("TGT PKT ITEM= 'new item'") # set_tlm("TGT PKT ITEM ='new item'") - split_string = text.split("=") - if len(split_string) < 2 or not split_string[1].strip(): + initial_split = text.split("=") + if len(initial_split) < 2 or not initial_split[1].strip(): raise RuntimeError(error_msg) - split_string = ( - split_string[0].strip().split(" ") + "=".join(split_string[1:]).strip() - ) - if len(split_string) != 4: # Ensure tgt,pkt,item,value + parts = initial_split[0].strip().split(" ") + [initial_split[1].strip()] + + if len(parts) != 4: # Ensure tgt,pkt,item,value raise RuntimeError(error_msg) - target_name = split_string[0] - packet_name = split_string[1] - item_name = split_string[2] - value = convert_to_value(split_string[3].strip()) + target_name = parts[0] + packet_name = parts[1] + item_name = parts[2] + value = convert_to_value(parts[3]) if isinstance(value, str): value = remove_quotes(value) return target_name, packet_name, item_name, value diff --git a/openc3/python/test/api/test_tlm_api.py b/openc3/python/test/api/test_tlm_api.py index 5904a548b8..4156a24be6 100644 --- a/openc3/python/test/api/test_tlm_api.py +++ b/openc3/python/test/api/test_tlm_api.py @@ -14,43 +14,968 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. -import json +import time +from datetime import datetime, timezone, timedelta import unittest +import threading from unittest.mock import * from test.test_helper import * from openc3.api.tlm_api import * -from openc3.utilities.store import Store -from openc3.packets.packet import Packet +from openc3.topics.telemetry_decom_topic import TelemetryDecomTopic +from openc3.topics.telemetry_topic import TelemetryTopic +from openc3.models.microservice_model import MicroserviceModel +from openc3.microservices.decom_microservice import DecomMicroservice +from openc3.utilities.time import formatted class TestTlmApi(unittest.TestCase): def setUp(self): - self.redis = mock_redis(self) + redis = mock_redis(self) + setup_system() - self.model = TargetModel(name="INST", scope="DEFAULT") - self.model.create() - hs = Packet("INST", "HEALTH_STATUS") - Store.hset( - "DEFAULT__openc3tlm__INST", "HEALTH_STATUS", json.dumps(hs.as_json()) - ) + self.process = True + orig_xread = redis.xread + + # Override xread to ignore the block and count keywords + def xread_side_effect(*args, **kwargs): + result = None + if self.process: + try: + result = orig_xread(*args) + except RuntimeError: + pass + + # # Create a slight delay to simulate the blocking call + if result and len(result) == 0: + time.sleep(0.01) + return result + + redis.xread = Mock() + redis.xread.side_effect = xread_side_effect + model = TargetModel(name="INST", scope="DEFAULT") + model.create() + model = TargetModel(name="SYSTEM", scope="DEFAULT") + model.create() + + packet = System.telemetry.packet("INST", "HEALTH_STATUS") + packet.received_time = datetime.now(timezone.utc) + packet.stored = False + packet.check_limits() + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + time.sleep(0.01) # Allow the write to happen + + # tlm, tlm_raw, tlm_formatted, tlm_with_units def test_tlm_complains_about_unknown_targets_commands_and_parameters(self): - with self.assertRaises(RuntimeError) as error: - tlm("BLAH HEALTH_STATUS COLLECTS") - self.assertTrue("does not exist") in error.exception - with self.assertRaises(RuntimeError) as error: - tlm("INST HEALTH_STATUS BLAH") - self.assertTrue("does not exist") in error.exception - with self.assertRaises(RuntimeError) as error: - tlm("BLAH", "HEALTH_STATUS", "COLLECTS") - self.assertTrue("does not exist") in error.exception - with self.assertRaises(RuntimeError) as error: - tlm("INST", "UNKNOWN", "COLLECTS") - self.assertTrue("does not exist") in error.exception - with self.assertRaises(RuntimeError) as error: - tlm("INST", "HEALTH_STATUS", "BLAH") - self.assertTrue("does not exist") in error.exception - - # def test_tlm_processes_a_string(self): - # print(self.redis) - # self.assertEqual(tlm("INST HEALTH_STATUS COLLECTS"), -100.0) + for name in [ + "tlm", + "tlm_raw", + "tlm_formatted", + "tlm_with_units", + ]: + func = globals()[name] + with self.assertRaisesRegex(RuntimeError, "does not exist"): + func("BLAH HEALTH_STATUS COLLECTS") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + func("INST HEALTH_STATUS BLAH") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + func("BLAH", "HEALTH_STATUS", "COLLECTS") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + func("INST", "UNKNOWN", "COLLECTS") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + func("INST", "HEALTH_STATUS", "BLAH") + + def test_processes_a_string(self): + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), -100.0) + self.assertEqual(tlm_raw("INST HEALTH_STATUS TEMP1"), 0) + self.assertEqual(tlm_formatted("INST HEALTH_STATUS TEMP1"), "-100.000") + self.assertEqual(tlm_with_units("INST HEALTH_STATUS TEMP1"), "-100.000 C") + + def test_processes_parameters(self): + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1"), -100.0) + self.assertEqual(tlm_raw("INST", "HEALTH_STATUS", "TEMP1"), 0) + self.assertEqual(tlm_formatted("INST", "HEALTH_STATUS", "TEMP1"), "-100.000") + self.assertEqual(tlm_with_units("INST", "HEALTH_STATUS", "TEMP1"), "-100.000 C") + + def test_complains_if_too_many_parameters(self): + with self.assertRaisesRegex(RuntimeError, "Invalid number of arguments"): + tlm("INST", "HEALTH_STATUS", "TEMP1", "TEMP2") + + def test_returns_the_value_using_latest(self): + now = datetime.now(timezone.utc) + packet = System.telemetry.packet("INST", "IMAGE") + packet.received_time = now + packet.write("CCSDSVER", 1) + json_hash = CvtModel.build_json_from_packet(packet) + CvtModel.set( + json_hash, + packet.target_name, + packet.packet_name, + scope="DEFAULT", + ) + packet = System.telemetry.packet("INST", "ADCS") + packet.received_time = now + timedelta(seconds=1) + packet.write("CCSDSVER", 2) + json_hash = CvtModel.build_json_from_packet(packet) + CvtModel.set( + json_hash, + packet.target_name, + packet.packet_name, + scope="DEFAULT", + ) + time.sleep(0.01) # Allow the writes to happen + self.assertEqual(tlm("INST LATEST CCSDSVER"), 2) + # Ensure case doesn't matter ... it still works + self.assertEqual(tlm("inst Latest CcsdsVER"), 2) + + # set_tlm + def test_set_tlm_complains_about_unknown_targets_packets_and_parameters(self): + with self.assertRaisesRegex(RuntimeError, "does not exist"): + set_tlm("BLAH HEALTH_STATUS COLLECTS = 1") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + set_tlm("INST UNKNOWN COLLECTS = 1") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + set_tlm("INST HEALTH_STATUS BLAH = 1") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + set_tlm("BLAH", "HEALTH_STATUS", "COLLECTS", 1) + with self.assertRaisesRegex(RuntimeError, "does not exist"): + set_tlm("INST", "UNKNOWN", "COLLECTS", 1) + with self.assertRaisesRegex(RuntimeError, "does not exist"): + set_tlm("INST", "HEALTH_STATUS", "BLAH", 1) + + def test_set_tlm_complains_with_too_many_parameters(self): + with self.assertRaisesRegex(RuntimeError, "Invalid number of arguments"): + set_tlm("INST", "HEALTH_STATUS", "TEMP1", "TEMP2", 0.0) + + def test_set_tlm_complains_with_unknown_types(self): + with self.assertRaisesRegex(RuntimeError, "Unknown type 'BLAH'"): + set_tlm("INST", "HEALTH_STATUS", "TEMP1", 0.0, type="BLAH") + + def test_set_tlm_processes_a_string(self): + set_tlm("inst Health_Status temp1 = 0.0") # match doesn't matter: + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), (0.0)) + set_tlm("INST HEALTH_STATUS TEMP1 = 100.0") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), (100.0)) + + def test_set_tlm_processes_parameters(self): + set_tlm("inst", "Health_Status", "Temp1", 0.0) # match doesn't matter: + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), (0.0)) + set_tlm("INST", "HEALTH_STATUS", "TEMP1", -50.0) + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), (-50.0)) + + def test_set_tlm_sets_raw_telemetry(self): + set_tlm("INST HEALTH_STATUS TEMP1 = 10.0", type="RAW") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1", type="RAW"), 10.0) + set_tlm("INST", "HEALTH_STATUS", "TEMP1", 0.0, type="RAW") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1", type="RAW"), 0.0) + set_tlm("INST HEALTH_STATUS ARY = [1,2,3]", type="RAW") + self.assertEqual(tlm("INST HEALTH_STATUS ARY", type="RAW"), [1, 2, 3]) + + def test_set_tlm_sets_converted_telemetry(self): + set_tlm("INST HEALTH_STATUS TEMP1 = 10.0", type="CONVERTED") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), 10.0) + set_tlm("INST", "HEALTH_STATUS", "TEMP1", 0.0, type="CONVERTED") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), 0.0) + set_tlm("INST HEALTH_STATUS ARY = [1,2,3]", type="CONVERTED") + self.assertEqual(tlm("INST HEALTH_STATUS ARY"), [1, 2, 3]) + + def test_set_tlm_sets_formatted_telemetry(self): + set_tlm("INST HEALTH_STATUS TEMP1 = '10.000'", type="FORMATTED") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1", type="FORMATTED"), "10.000") + set_tlm("INST", "HEALTH_STATUS", "TEMP1", 0.0, type="FORMATTED") # Float + self.assertEqual( + tlm("INST HEALTH_STATUS TEMP1", type="FORMATTED"), "0.0" + ) # String + set_tlm("INST HEALTH_STATUS ARY = '[1,2,3]'", type="FORMATTED") + self.assertEqual(tlm("INST HEALTH_STATUS ARY", type="FORMATTED"), "[1,2,3]") + + def test_set_tlm_sets_with_units_telemetry(self): + set_tlm("INST HEALTH_STATUS TEMP1 = '10.0 C'", type="WITH_UNITS") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1", type="WITH_UNITS"), "10.0 C") + set_tlm("INST", "HEALTH_STATUS", "TEMP1", 0.0, type="WITH_UNITS") # Float + self.assertEqual( + tlm("INST HEALTH_STATUS TEMP1", type="WITH_UNITS"), "0.0" + ) # String + set_tlm("INST HEALTH_STATUS ARY = '[1,2,3]'", type="WITH_UNITS") + self.assertEqual(tlm("INST HEALTH_STATUS ARY", type="WITH_UNITS"), "[1,2,3]") + + def decom_stuff(self): + model = MicroserviceModel( + name="DEFAULT__DECOM__INST_INT", + scope="DEFAULT", + topics=[ + "DEFAULT__TELEMETRY__{INST}__HEALTH_STATUS", + "DEFAULT__TELEMETRY__{SYSTEM}__META", + ], + target_names=["INST"], + ) + model.create() + self.dm = DecomMicroservice("DEFAULT__DECOM__INST_INT") + self.dm_thread = threading.Thread(target=self.dm.run) + self.dm_thread.start() + packet = System.telemetry.packet("INST", "HEALTH_STATUS") + TelemetryTopic.write_packet(packet, scope="DEFAULT") + time.sleep(0.001) + + def test_inject_tlm_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" + ): + inject_tlm("BLAH", "HEALTH_STATUS") + + def test_inject_tlm_complains_about_non_existant_packets(self): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): + inject_tlm("INST", "BLAH") + + def test_inject_tlm_complains_about_non_existant_items(self): + with self.assertRaisesRegex( + RuntimeError, "Item\(s\) 'INST HEALTH_STATUS BLAH' does not exist" + ): + inject_tlm("INST", "HEALTH_STATUS", {"BLAH": 0}) + + def test_inject_tlm_complains_about_bad_types(self): + with self.assertRaisesRegex(RuntimeError, "Unknown type 'BLAH'"): + inject_tlm("INST", "HEALTH_STATUS", {"TEMP1": 0}, type="BLAH") + + @patch("openc3.microservices.microservice.System") + def test_inject_tlm_injects_a_packet_into_target_without_an_interface( + self, mock_system + ): + self.decom_stuff() + # Case doesn't matter + inject_tlm( + "inst", "Health_Status", {"temp1": 10, "Temp2": 20}, type="CONVERTED" + ) + time.sleep(0.01) + self.assertAlmostEqual(tlm("INST HEALTH_STATUS TEMP1"), 10.0, delta=0.1) + self.assertAlmostEqual(tlm("INST HEALTH_STATUS TEMP2"), 20.0, delta=0.1) + + inject_tlm("INST", "HEALTH_STATUS", {"TEMP1": 0, "TEMP2": 0}, type="RAW") + time.sleep(0.01) + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), (-100.0)) + self.assertEqual(tlm("INST HEALTH_STATUS TEMP2"), (-100.0)) + + self.dm.shutdown() + + @patch("openc3.microservices.microservice.System") + def test_inject_tlm_bumps_the_received_count(self, mock_system): + self.decom_stuff() + + inject_tlm("INST", "HEALTH_STATUS") + time.sleep(0.01) + self.assertEqual(tlm("INST HEALTH_STATUS RECEIVED_COUNT"), 1) + inject_tlm("INST", "HEALTH_STATUS") + time.sleep(0.01) + self.assertEqual(tlm("INST HEALTH_STATUS RECEIVED_COUNT"), 2) + inject_tlm("INST", "HEALTH_STATUS") + time.sleep(0.01) + self.assertEqual(tlm("INST HEALTH_STATUS RECEIVED_COUNT"), 3) + + self.dm.shutdown() + + # override_tlm + def test_overrides_complains_about_unknown_targets_packets_and_parameters(self): + with self.assertRaisesRegex(RuntimeError, "does not exist"): + override_tlm("BLAH HEALTH_STATUS COLLECTS = 1") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + override_tlm("INST UNKNOWN COLLECTS = 1") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + override_tlm("INST HEALTH_STATUS BLAH = 1") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + override_tlm("BLAH", "HEALTH_STATUS", "COLLECTS", 1) + with self.assertRaisesRegex(RuntimeError, "does not exist"): + override_tlm("INST", "UNKNOWN", "COLLECTS", 1) + with self.assertRaisesRegex(RuntimeError, "does not exist"): + override_tlm("INST", "HEALTH_STATUS", "BLAH", 1) + + def test_overrides_complains_with_too_many_parameters(self): + with self.assertRaisesRegex(RuntimeError, "Invalid number of arguments"): + override_tlm("INST", "HEALTH_STATUS", "TEMP1", "TEMP2", 0.0) + + def test_overrides_all_values(self): + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), (0)) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="CONVERTED"), -100.0 + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), ("-100.000") + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), ("-100.000 C") + ) + # Case doesn't matter + override_tlm("inst Health_Status Temp1 = 10") + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), (10)) + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="CONVERTED"), (10)) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), ("10") + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), ("10") + ) + override_tlm("INST", "HEALTH_STATUS", "TEMP1", 5.0) # other syntax + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), (5.0)) + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="CONVERTED"), (5.0)) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), ("5.0") + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), ("5.0") + ) + # NOTE: As a user you can override with weird values and this is allowed + override_tlm("INST", "HEALTH_STATUS", "TEMP1", "what?") + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), ("what?")) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="CONVERTED"), ("what?") + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), ("what?") + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), ("what?") + ) + normalize_tlm("INST HEALTH_STATUS TEMP1") + + def test_overrides_all_array_values(self): + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "ARY", type="RAW"), + ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "ARY", type="CONVERTED"), + ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "ARY", type="FORMATTED"), + ("[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]"), + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "ARY", type="WITH_UNITS"), + ("['0 V', '0 V', '0 V', '0 V', '0 V', '0 V', '0 V', '0 V', '0 V', '0 V']"), + ) + override_tlm("INST HEALTH_STATUS ARY = [1,2,3]") + self.assertEqual(tlm("INST", "HEALTH_STATUS", "ARY", type="RAW"), ([1, 2, 3])) + self.assertEqual(tlm("INST", "HEALTH_STATUS", "ARY"), ([1, 2, 3])) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "ARY", type="FORMATTED"), ("[1, 2, 3]") + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "ARY", type="WITH_UNITS"), ("[1, 2, 3]") + ) # NOTE: 'V' not applied + normalize_tlm("INST HEALTH_STATUS ARY") + + def test_overrides_raw_values(self): + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), 0) + override_tlm("INST", "HEALTH_STATUS", "TEMP1", 5.0, type="RAW") + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), 5.0) + set_tlm("INST", "HEALTH_STATUS", "TEMP1", 10.0, type="RAW") + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), 5.0) + normalize_tlm("INST HEALTH_STATUS TEMP1") + + def test_overrides_converted_values(self): + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), -100.0) + override_tlm("INST HEALTH_STATUS TEMP1 = 60.0", type="CONVERTED") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), 60.0) + override_tlm("INST HEALTH_STATUS TEMP1 = 50.0", type="CONVERTED") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), 50.0) + set_tlm("INST HEALTH_STATUS TEMP1 = 10.0") + self.assertEqual(tlm("INST HEALTH_STATUS TEMP1"), 50.0) + normalize_tlm("INST HEALTH_STATUS TEMP1") + + def test_overrides_formatted_values(self): + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), "-100.000" + ) + override_tlm("INST", "HEALTH_STATUS", "TEMP1", "5.000", type="FORMATTED") + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), "5.000" + ) + set_tlm("INST", "HEALTH_STATUS", "TEMP1", "10.000", type="FORMATTED") + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), "5.000" + ) + normalize_tlm("INST HEALTH_STATUS TEMP1") + + def test_overrides_with_units_values(self): + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), "-100.000 C" + ) + override_tlm("INST", "HEALTH_STATUS", "TEMP1", "5.00 C", type="WITH_UNITS") + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), "5.00 C" + ) + set_tlm("INST", "HEALTH_STATUS", "TEMP1", 10.0, type="WITH_UNITS") + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), "5.00 C" + ) + normalize_tlm("INST HEALTH_STATUS TEMP1") + + # get_overrides + def test_get_overrides_returns_empty_array_with_no_overrides(self): + self.assertEqual(get_overrides(), ([])) + + def test_returns_all_overrides(self): + override_tlm("INST HEALTH_STATUS temp1 = 10") + override_tlm("INST HEALTH_STATUS ARY = [1,2,3]", type="RAW") + overrides = get_overrides() + self.assertEqual(len(overrides), 5) # 4 for TEMP1 and 1 for ARY + self.assertEqual( + overrides[0], + ( + { + "target_name": "INST", + "packet_name": "HEALTH_STATUS", + "item_name": "TEMP1", + "value_type": "RAW", + "value": 10, + } + ), + ) + self.assertEqual( + overrides[1], + ( + { + "target_name": "INST", + "packet_name": "HEALTH_STATUS", + "item_name": "TEMP1", + "value_type": "CONVERTED", + "value": 10, + } + ), + ) + self.assertEqual( + overrides[2], + ( + { + "target_name": "INST", + "packet_name": "HEALTH_STATUS", + "item_name": "TEMP1", + "value_type": "FORMATTED", + "value": "10", + } + ), + ) + self.assertEqual( + overrides[3], + ( + { + "target_name": "INST", + "packet_name": "HEALTH_STATUS", + "item_name": "TEMP1", + "value_type": "WITH_UNITS", + "value": "10", + } + ), + ) + self.assertEqual( + overrides[4], + ( + { + "target_name": "INST", + "packet_name": "HEALTH_STATUS", + "item_name": "ARY", + "value_type": "RAW", + "value": [1, 2, 3], + } + ), + ) + + # normalize_tlm + def test_normalize_tlm_complains_about_unknown_targets_packets_and_parameters(self): + with self.assertRaisesRegex(RuntimeError, "does not exist"): + normalize_tlm("BLAH HEALTH_STATUS COLLECTS") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + normalize_tlm("INST UNKNOWN COLLECTS") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + normalize_tlm("INST HEALTH_STATUS BLAH") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + normalize_tlm("BLAH", "HEALTH_STATUS", "COLLECTS") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + normalize_tlm("INST", "UNKNOWN", "COLLECTS") + with self.assertRaisesRegex(RuntimeError, "does not exist"): + normalize_tlm("INST", "HEALTH_STATUS", "BLAH") + + def test_normalize_tlm_complains_with_too_many_parameters(self): + with self.assertRaisesRegex(RuntimeError, "Invalid number of arguments"): + normalize_tlm("INST", "HEALTH_STATUS", "TEMP1", "TEMP2") + + def test_normalize_tlm_clears_all_overrides(self): + override_tlm("INST", "HEALTH_STATUS", "TEMP1", 5.0, type="RAW") + override_tlm("INST", "HEALTH_STATUS", "TEMP1", 50.0, type="CONVERTED") + override_tlm("INST", "HEALTH_STATUS", "TEMP1", "50.00", type="FORMATTED") + override_tlm("INST", "HEALTH_STATUS", "TEMP1", "50.00 F", type="WITH_UNITS") + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), (5.0)) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="CONVERTED"), (50.0) + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), ("50.00") + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), ("50.00 F") + ) + normalize_tlm("INST", "HEALTH_STATUS", "temp1") + self.assertEqual(tlm("INST", "HEALTH_STATUS", "TEMP1", type="RAW"), (0)) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="CONVERTED"), -100.0 + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="FORMATTED"), ("-100.000") + ) + self.assertEqual( + tlm("INST", "HEALTH_STATUS", "TEMP1", type="WITH_UNITS"), ("-100.000 C") + ) + + # get_tlm_buffer + def test_get_tlm_buffer_returns_a_telemetry_packet_buffer(self): + buffer = b"\x01\x02\x03\x04" + packet = System.telemetry.packet("INST", "HEALTH_STATUS") + packet.buffer = buffer + TelemetryTopic.write_packet(packet, scope="DEFAULT") + output = get_tlm_buffer("INST", "Health_Status") + self.assertEqual(output["buffer"][0:4], buffer) + + def test_get_tlm_buffer_returns_none_for_no_current_packet(self): + output = get_tlm_buffer("INST", "MECH") + self.assertIsNone(output) + + # get_all_telemetry + def test_get_all_telemetry_raises_if_the_target_does_not_exist(self): + with self.assertRaisesRegex(RuntimeError, "Target 'BLAH' does not exist"): + get_all_telemetry("BLAH", scope="DEFAULT") + + def test_get_all_telemetry_returns_an_array_of_all_packet_hashes(self): + pkts = get_all_telemetry("inst", scope="DEFAULT") + self.assertEqual(type(pkts), list) + names = [] + for pkt in pkts: + self.assertEqual(type(pkt), dict) + self.assertEqual(pkt["target_name"], "INST") + names.append(pkt["packet_name"]) + self.assertIn("ADCS", names) + self.assertIn("HEALTH_STATUS", names) + self.assertIn("PARAMS", names) + self.assertIn("IMAGE", names) + self.assertIn("MECH", names) + + def test_get_all_telemetry_names_returns_an_empty_array_if_the_target_does_not_exist( + self, + ): + self.assertEqual(get_all_telemetry_names("BLAH"), []) + + def test_get_all_telemetry_names_returns_an_array_of_all_packet_names(self): + pkts = get_all_telemetry_names("inst", scope="DEFAULT") + self.assertEqual(type(pkts), list) + self.assertEqual(type(pkts[0]), str) + + # get_telemetry + def test_get_telemetry_raises_if_the_target_or_packet_do_not_exist(self): + with self.assertRaisesRegex( + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" + ): + get_telemetry("BLAH", "HEALTH_STATUS", scope="DEFAULT") + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): + get_telemetry("INST", "BLAH", scope="DEFAULT") + + def test_get_telemetry_returns_a_packet_hash(self): + pkt = get_telemetry("inst", "Health_Status", scope="DEFAULT") + self.assertEqual(type(pkt), dict) + self.assertEqual(pkt["target_name"], "INST") + self.assertEqual(pkt["packet_name"], "HEALTH_STATUS") + + # get_item + def test_get_item_raises_if_the_target_or_packet_or_item_do_not_exist(self): + with self.assertRaisesRegex( + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" + ): + get_item("BLAH", "HEALTH_STATUS", "CCSDSVER", scope="DEFAULT") + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): + get_item("INST", "BLAH", "CCSDSVER", scope="DEFAULT") + with self.assertRaisesRegex( + RuntimeError, "Item 'INST HEALTH_STATUS BLAH' does not exist" + ): + get_item("INST", "HEALTH_STATUS", "BLAH", scope="DEFAULT") + + def test_get_item_returns_an_item_hash(self): + item = get_item("inst", "Health_Status", "CcsdsVER", scope="DEFAULT") + self.assertEqual(type(item), dict) + self.assertEqual(item["name"], "CCSDSVER") + self.assertEqual(item["bit_offset"], 0) + + # get_tlm_packet + def test_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" + ): + get_tlm_packet("BLAH", "HEALTH_STATUS") + + def test_complains_about_non_existant_packets(self): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): + get_tlm_packet("INST", "BLAH") + + def test_complains_using_latest(self): + with self.assertRaisesRegex( + RuntimeError, "Packet 'INST LATEST' does not exist" + ): + get_tlm_packet("INST", "LATEST") + + def test_complains_about_non_existant_value_types(self): + with self.assertRaisesRegex( + AttributeError, "Unknown type 'MINE' for INST HEALTH_STATUS" + ): + get_tlm_packet("INST", "HEALTH_STATUS", type="MINE") + + def test_reads_all_telemetry_items_as_converted_with_their_limits_states(self): + vals = get_tlm_packet("inst", "Health_Status") + # Spot check a few + self.assertEqual(vals[11][0], "TEMP1") + self.assertEqual(vals[11][1], -100.0) + self.assertEqual(vals[11][2], "RED_LOW") + self.assertEqual(vals[12][0], "TEMP2") + self.assertEqual(vals[12][1], -100.0) + self.assertEqual(vals[12][2], "RED_LOW") + self.assertEqual(vals[13][0], "TEMP3") + self.assertEqual(vals[13][1], -100.0) + self.assertEqual(vals[13][2], "RED_LOW") + self.assertEqual(vals[14][0], "TEMP4") + self.assertEqual(vals[14][1], -100.0) + self.assertEqual(vals[14][2], "RED_LOW") + # Derived items are last + self.assertEqual(vals[23][0], "PACKET_TIMESECONDS") + self.assertGreater(vals[23][1], 0) + self.assertIsNone(vals[23][2]) + self.assertEqual(vals[24][0], "PACKET_TIMEFORMATTED") + self.assertEqual( + vals[24][1].split(" ")[0], + formatted(datetime.now(timezone.utc)).split(" ")[0], + ) # Match the date + self.assertIsNone(vals[24][2]) + self.assertEqual(vals[25][0], "RECEIVED_TIMESECONDS") + self.assertGreater(vals[25][1], 0) + self.assertIsNone(vals[25][2]) + self.assertEqual(vals[26][0], "RECEIVED_TIMEFORMATTED") + self.assertEqual( + vals[26][1].split(" ")[0], + formatted(datetime.now(timezone.utc)).split(" ")[0], + ) # Match the date + self.assertIsNone(vals[26][2]) + self.assertEqual(vals[27][0], "RECEIVED_COUNT") + self.assertEqual(vals[27][1], 0) + self.assertIsNone(vals[27][2]) + + def test_reads_all_telemetry_items_as_raw(self): + vals = get_tlm_packet("INST", "HEALTH_STATUS", type="RAW") + self.assertEqual(vals[11][0], "TEMP1") + self.assertEqual(vals[11][1], 0) + self.assertEqual(vals[11][2], "RED_LOW") + self.assertEqual(vals[12][0], "TEMP2") + self.assertEqual(vals[12][1], 0) + self.assertEqual(vals[12][2], "RED_LOW") + self.assertEqual(vals[13][0], "TEMP3") + self.assertEqual(vals[13][1], 0) + self.assertEqual(vals[13][2], "RED_LOW") + self.assertEqual(vals[14][0], "TEMP4") + self.assertEqual(vals[14][1], 0) + self.assertEqual(vals[14][2], "RED_LOW") + + def test_reads_all_telemetry_items_as_formatted(self): + vals = get_tlm_packet("INST", "HEALTH_STATUS", type="FORMATTED") + self.assertEqual(vals[11][0], "TEMP1") + self.assertEqual(vals[11][1], "-100.000") + self.assertEqual(vals[11][2], "RED_LOW") + self.assertEqual(vals[12][0], "TEMP2") + self.assertEqual(vals[12][1], "-100.000") + self.assertEqual(vals[12][2], "RED_LOW") + self.assertEqual(vals[13][0], "TEMP3") + self.assertEqual(vals[13][1], "-100.000") + self.assertEqual(vals[13][2], "RED_LOW") + self.assertEqual(vals[14][0], "TEMP4") + self.assertEqual(vals[14][1], "-100.000") + self.assertEqual(vals[14][2], "RED_LOW") + + def test_reads_all_telemetry_items_as_with_units(self): + vals = get_tlm_packet("INST", "HEALTH_STATUS", type="WITH_UNITS") + self.assertEqual(vals[11][0], "TEMP1") + self.assertEqual(vals[11][1], "-100.000 C") + self.assertEqual(vals[11][2], "RED_LOW") + self.assertEqual(vals[12][0], "TEMP2") + self.assertEqual(vals[12][1], "-100.000 C") + self.assertEqual(vals[12][2], "RED_LOW") + self.assertEqual(vals[13][0], "TEMP3") + self.assertEqual(vals[13][1], "-100.000 C") + self.assertEqual(vals[13][2], "RED_LOW") + self.assertEqual(vals[14][0], "TEMP4") + self.assertEqual(vals[14][1], "-100.000 C") + self.assertEqual(vals[14][2], "RED_LOW") + + def test_marks_data_as_stale(self): + packet = System.telemetry.packet("INST", "HEALTH_STATUS") + packet.received_time = datetime.now(timezone.utc) - timedelta(seconds=100) + packet.stored = False + packet.check_limits() + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + time.sleep(0.01) # Allow the write to happen + + # Use the default stale_time of 30s + vals = get_tlm_packet("INST", "HEALTH_STATUS") + # Spot check a few + self.assertEqual(vals[11][0], "TEMP1") + self.assertEqual(vals[11][1], -100.0) + self.assertEqual(vals[11][2], "STALE") + self.assertEqual(vals[12][0], "TEMP2") + self.assertEqual(vals[12][1], -100.0) + self.assertEqual(vals[12][2], "STALE") + self.assertEqual(vals[13][0], "TEMP3") + self.assertEqual(vals[13][1], -100.0) + self.assertEqual(vals[13][2], "STALE") + self.assertEqual(vals[14][0], "TEMP4") + self.assertEqual(vals[14][1], -100.0) + self.assertEqual(vals[14][2], "STALE") + + vals = get_tlm_packet("INST", "HEALTH_STATUS", stale_time=101) + # Verify it goes back to the limits setting and not STALE + self.assertEqual(vals[11][0], "TEMP1") + self.assertEqual(vals[11][1], -100.0) + self.assertEqual(vals[11][2], "RED_LOW") + self.assertEqual(vals[12][0], "TEMP2") + self.assertEqual(vals[12][1], -100.0) + self.assertEqual(vals[12][2], "RED_LOW") + self.assertEqual(vals[13][0], "TEMP3") + self.assertEqual(vals[13][1], -100.0) + self.assertEqual(vals[13][2], "RED_LOW") + self.assertEqual(vals[14][0], "TEMP4") + self.assertEqual(vals[14][1], -100.0) + self.assertEqual(vals[14][2], "RED_LOW") + + # get_tlm_values + def test_get_tlm_values_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" + ): + get_tlm_values(["BLAH__HEALTH_STATUS__TEMP1__CONVERTED"]) + + def test_get_tlm_values_complains_about_non_existant_packets(self): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): + get_tlm_values(["INST__BLAH__TEMP1__CONVERTED"]) + + def test_get_tlm_values_complains_about_non_existant_items(self): + with self.assertRaisesRegex( + RuntimeError, "Item 'INST HEALTH_STATUS BLAH' does not exist" + ): + get_tlm_values(["INST__HEALTH_STATUS__BLAH__CONVERTED"]) + with self.assertRaisesRegex( + RuntimeError, "Item 'INST LATEST BLAH' does not exist for scope: DEFAULT" + ): + get_tlm_values(["INST__LATEST__BLAH__CONVERTED"]) + + def test_get_tlm_values_complains_about_non_existant_value_types(self): + with self.assertRaisesRegex(RuntimeError, "Unknown value type 'MINE'"): + get_tlm_values(["INST__HEALTH_STATUS__TEMP1__MINE"]) + + def test_get_tlm_values_complains_about_bad_arguments(self): + with self.assertRaises(TypeError): + get_tlm_values() + with self.assertRaisesRegex(AttributeError, "items must be array of strings"): + get_tlm_values([]) + with self.assertRaisesRegex(AttributeError, "items must be array of strings"): + get_tlm_values([["INST", "HEALTH_STATUS", "TEMP1"]]) + with self.assertRaisesRegex(AttributeError, "items must be formatted"): + get_tlm_values(["INST", "HEALTH_STATUS", "TEMP1"]) + + def test_get_tlm_values_reads_all_the_specified_items(self): + items = [] + items.append("inst__Health_Status__Temp1__converted") # Case doesn't matter + items.append("INST__LATEST__TEMP2__CONVERTED") + items.append("INST__HEALTH_STATUS__TEMP3__CONVERTED") + items.append("INST__LATEST__TEMP4__CONVERTED") + items.append("INST__HEALTH_STATUS__DURATION__CONVERTED") + vals = get_tlm_values(items) + self.assertEqual(vals[0][0], (-100.0)) + self.assertEqual(vals[1][0], (-100.0)) + self.assertEqual(vals[2][0], (-100.0)) + self.assertEqual(vals[3][0], (-100.0)) + self.assertEqual(vals[4][0], (0.0)) + self.assertEqual(vals[0][1], "RED_LOW") + self.assertEqual(vals[1][1], "RED_LOW") + self.assertEqual(vals[2][1], "RED_LOW") + self.assertEqual(vals[3][1], "RED_LOW") + self.assertIsNone(vals[4][1]) + + def test_get_tlm_values_reads_all_the_specified_raw_items(self): + items = [] + items.append("INST__HEALTH_STATUS__TEMP1__RAW") + items.append("INST__HEALTH_STATUS__TEMP2__RAW") + items.append("INST__HEALTH_STATUS__TEMP3__RAW") + items.append("INST__HEALTH_STATUS__TEMP4__RAW") + vals = get_tlm_values(items) + self.assertEqual(vals[0][0], 0) + self.assertEqual(vals[1][0], 0) + self.assertEqual(vals[2][0], 0) + self.assertEqual(vals[3][0], 0) + self.assertEqual(vals[0][1], "RED_LOW") + self.assertEqual(vals[1][1], "RED_LOW") + self.assertEqual(vals[2][1], "RED_LOW") + self.assertEqual(vals[3][1], "RED_LOW") + + def test_get_tlm_values_reads_all_the_specified_items_with_different_conversions( + self, + ): + items = [] + items.append("INST__HEALTH_STATUS__TEMP1__RAW") + items.append("INST__HEALTH_STATUS__TEMP2__CONVERTED") + items.append("INST__HEALTH_STATUS__TEMP3__FORMATTED") + items.append("INST__HEALTH_STATUS__TEMP4__WITH_UNITS") + vals = get_tlm_values(items) + self.assertEqual(vals[0][0], 0) + self.assertEqual(vals[1][0], (-100.0)) + self.assertEqual(vals[2][0], "-100.000") + self.assertEqual(vals[3][0], "-100.000 C") + self.assertEqual(vals[0][1], "RED_LOW") + self.assertEqual(vals[1][1], "RED_LOW") + self.assertEqual(vals[2][1], "RED_LOW") + self.assertEqual(vals[3][1], "RED_LOW") + + def test_get_tlm_values_returns_even_when_requesting_items_that_do_not_yet_exist_in_cvt( + self, + ): + items = [] + items.append("INST__HEALTH_STATUS__TEMP1__CONVERTED") + items.append("INST__PARAMS__VALUE1__CONVERTED") + items.append("INST__MECH__SLRPNL1__CONVERTED") + items.append("INST__ADCS__POSX__CONVERTED") + vals = get_tlm_values(items) + self.assertEqual(vals[0][0], (-100.0)) + self.assertIsNone(vals[1][0]) + self.assertIsNone(vals[2][0]) + self.assertIsNone(vals[3][0]) + self.assertEqual(vals[0][1], "RED_LOW") + self.assertIsNone(vals[1][1]) + self.assertIsNone(vals[2][1]) + self.assertIsNone(vals[3][1]) + + def test_get_tlm_values_handles_block_data_as_binary(self): + items = [] + items.append("INST__HEALTH_STATUS__BLOCKTEST__RAW") + vals = get_tlm_values(items) + self.assertEqual(vals[0][0], b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + self.assertIsNone(vals[0][1]) + + def test_get_tlm_values_marks_data_as_stale(self): + packet = System.telemetry.packet("INST", "HEALTH_STATUS") + packet.received_time = datetime.now(timezone.utc) - timedelta(seconds=100) + packet.stored = False + packet.check_limits() + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + time.sleep(0.01) # Allow the write to happen + + items = [] + items.append("INST__HEALTH_STATUS__TEMP1__CONVERTED") + items.append("INST__LATEST__TEMP2__CONVERTED") + items.append("INST__HEALTH_STATUS__TEMP3__CONVERTED") + items.append("INST__LATEST__TEMP4__CONVERTED") + items.append("INST__HEALTH_STATUS__DURATION__CONVERTED") + # Use the default stale_time of 30s + vals = get_tlm_values(items) + self.assertEqual(vals[0][0], -100.0) + self.assertEqual(vals[1][0], -100.0) + self.assertEqual(vals[2][0], -100.0) + self.assertEqual(vals[3][0], -100.0) + self.assertEqual(vals[4][0], 0.0) + self.assertEqual(vals[0][1], "STALE") + self.assertEqual(vals[1][1], "STALE") + self.assertEqual(vals[2][1], "STALE") + self.assertEqual(vals[3][1], "STALE") + self.assertEqual(vals[4][1], "STALE") + + vals = get_tlm_values(items, stale_time=101) + self.assertEqual(vals[0][0], -100.0) + self.assertEqual(vals[1][0], -100.0) + self.assertEqual(vals[2][0], -100.0) + self.assertEqual(vals[3][0], -100.0) + self.assertEqual(vals[4][0], 0.0) + self.assertEqual(vals[0][1], "RED_LOW") + self.assertEqual(vals[1][1], "RED_LOW") + self.assertEqual(vals[2][1], "RED_LOW") + self.assertEqual(vals[3][1], "RED_LOW") + self.assertIsNone(vals[4][1]) + + # def test_streams_packets_since_the_subscription_was_created(self): + # # Write an initial packet that should not be returned + # packet = System.telemetry.packet("INST", "HEALTH_STATUS") + # packet.received_time = datetime.now(timezone.utc) + # packet.write("DURATION", 1.0) + # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + # time.sleep(0.01) + + # id = subscribe_packets([["inst", "Health_Status"], ["INST", "ADCS"]]) + # time.sleep(0.01) + + # # Write some packets that should be returned and one that will not + # packet.received_time = datetime.now(timezone.utc) + # packet.write("DURATION", 2.0) + # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + # packet.received_time = datetime.now(timezone.utc) + # packet.write("DURATION", 3.0) + # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + # packet = System.telemetry.packet("INST", "ADCS") + # packet.received_time = datetime.now(timezone.utc) + # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + # packet = System.telemetry.packet("INST", "IMAGE") # Not subscribed + # packet.received_time = datetime.now(timezone.utc) + # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + + # id, packets = get_packets(id) + # packets.each_with_index do |packet, index| + # self.assertEqual(packet['target_name'], "INST") + # match index: + # case 0: + # self.assertEqual(packet['packet_name'], "HEALTH_STATUS") + # self.assertEqual(packet['DURATION'], 2.0) + # case 1: + # self.assertEqual(packet['packet_name'], "HEALTH_STATUS") + # self.assertEqual(packet['DURATION'], 3.0) + # case 2: + # self.assertEqual(packet['packet_name'], "ADCS") + # else: + # raise "Found too many packets" + + def test_get_tlm_cnt_complains_about_non_existant_targets(self): + with self.assertRaisesRegex(RuntimeError, "Packet 'BLAH ABORT' does not exist"): + get_tlm_cnt("BLAH", "ABORT") + + def test_get_tlm_cnt_complains_about_non_existant_packets(self): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): + get_tlm_cnt("INST", "BLAH") + + def test_get_tlm_cnt_returns_the_receive_count(self): + start = get_tlm_cnt("inst", "Health_Status") + + packet = System.telemetry.packet("INST", "HEALTH_STATUS").clone() + packet.received_time = datetime.now(timezone.utc) + packet.received_count += 1 + TelemetryTopic.write_packet(packet, scope="DEFAULT") + + count = get_tlm_cnt("INST", "HEALTH_STATUS") + self.assertEqual(count, start + 1) + + def test_get_tlm_cnts_returns_receive_counts_for_telemetry_packets(self): + packet = System.telemetry.packet("INST", "ADCS").clone() + packet.received_time = datetime.now(timezone.utc) + packet.received_count = 100 # This is what is used in the result + TelemetryTopic.write_packet(packet, scope="DEFAULT") + cnts = get_tlm_cnts([["inst", "Adcs"]]) + self.assertEqual(cnts, ([100])) + + def test_get_packet_derived_items_complains_about_non_existant_targets(self): + with self.assertRaisesRegex(RuntimeError, "Packet 'BLAH ABORT' does not exist"): + get_packet_derived_items("BLAH", "ABORT") + + def test_get_packet_derived_items_complains_about_non_existant_packets(self): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): + get_packet_derived_items("INST", "BLAH") + + def test_get_packet_derived_items_returns_the_packet_derived_items(self): + items = get_packet_derived_items("inst", "Health_Status") + self.assertIn("RECEIVED_TIMESECONDS", items) + self.assertIn("RECEIVED_TIMEFORMATTED", items) + self.assertIn("RECEIVED_COUNT", items) diff --git a/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_tlm.txt b/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_tlm.txt index bd77ade33c..1e723dae2a 100644 --- a/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_tlm.txt +++ b/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_tlm.txt @@ -153,6 +153,21 @@ TELEMETRY INST PARAMS BIG_ENDIAN "Params set by SETPARAMS command" ITEM TIMESEC 48 32 UINT "Seconds since epoch (January 1st, 1970, midnight)" ITEM TIMEUS 80 32 UINT "Microseconds of second" ID_ITEM PKTID 112 16 UINT 1 "Packet id (The combination of CCSDS_APID and PACKET_ID identify the packet)" + APPEND_ITEM VALUE1 16 UINT "Value setting" + STATE GOOD 0 GREEN + STATE BAD 1 RED + APPEND_ITEM VALUE2 16 UINT "Value setting" + STATE GOOD 0 GREEN + STATE BAD 1 RED + APPEND_ITEM VALUE3 16 UINT "Value setting" + STATE GOOD 0 GREEN + STATE BAD 1 RED + APPEND_ITEM VALUE4 16 UINT "Value setting" + STATE GOOD 0 GREEN + STATE BAD 1 RED + APPEND_ITEM VALUE5 16 UINT "Value setting" + STATE GOOD 0 GREEN + STATE BAD 1 RED TELEMETRY INST IMAGE BIG_ENDIAN "Packet with image data" ITEM CCSDSVER 0 3 UINT "CCSDS packet version number (See CCSDS 133.0-B-1)" diff --git a/openc3/python/test/microservices/test_interface_microservice.py b/openc3/python/test/microservices/test_interface_microservice.py index 8d6047cf62..7ed588868a 100644 --- a/openc3/python/test/microservices/test_interface_microservice.py +++ b/openc3/python/test/microservices/test_interface_microservice.py @@ -133,8 +133,8 @@ def setUp(self): json_hash = CvtModel.build_json_from_packet(packet) CvtModel.set( json_hash, - target_name=packet.target_name, - packet_name=packet.packet_name, + packet.target_name, + packet.packet_name, scope="DEFAULT", ) diff --git a/openc3/python/test/models/test_cvt_model.py b/openc3/python/test/models/test_cvt_model.py index b2fbdff5d0..99178902b8 100644 --- a/openc3/python/test/models/test_cvt_model.py +++ b/openc3/python/test/models/test_cvt_model.py @@ -94,8 +94,8 @@ def test_decoms_and_sets(self): json_hash = CvtModel.build_json_from_packet(packet) CvtModel.set( json_hash, - target_name=packet.target_name, - packet_name=packet.packet_name, + packet.target_name, + packet.packet_name, scope="DEFAULT", ) diff --git a/openc3/python/test/test_helper.py b/openc3/python/test/test_helper.py index a4046fe511..f73ea28046 100644 --- a/openc3/python/test/test_helper.py +++ b/openc3/python/test/test_helper.py @@ -26,6 +26,7 @@ import json import fakeredis from unittest.mock import * +from openc3.models.cvt_model import CvtModel from openc3.utilities.logger import Logger from openc3.utilities.store import Store, EphemeralStore from openc3.system.system import System @@ -49,6 +50,18 @@ def setup_system(targets=["SYSTEM", "INST", "EMPTY"]): packet_name, json.dumps(packet.as_json()), ) + packet = System.telemetry.packet(target_name, packet_name) + # packet.received_time = datetime.now(timezone.utc) + json_hash = {} + for item in packet.sorted_items: + # Initialize all items to None like TargetModel::update_store does in Ruby + json_hash[item.name] = None + CvtModel.set( + json_hash, # CvtModel.build_json_from_packet(packet), + packet.target_name, + packet.packet_name, + scope="DEFAULT", + ) except RuntimeError: pass try: From 46f587704498cc6162c2985b45259b0952eade26 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Thu, 12 Oct 2023 12:45:39 -0600 Subject: [PATCH 02/17] Revert remove token --- openc3/lib/openc3/api/tlm_api.rb | 92 ++++++++++++++++---------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/openc3/lib/openc3/api/tlm_api.rb b/openc3/lib/openc3/api/tlm_api.rb index 7b752fa55c..6dd564532a 100644 --- a/openc3/lib/openc3/api/tlm_api.rb +++ b/openc3/lib/openc3/api/tlm_api.rb @@ -66,27 +66,27 @@ module Api # @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, cache_timeout: 0.1, scope: $openc3_scope) + def tlm(*args, type: :CONVERTED, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) target_name, packet_name, item_name = tlm_process_args(args, 'tlm', cache_timeout: cache_timeout, scope: scope) - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, 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, cache_timeout: cache_timeout, scope: scope) end - def tlm_raw(*args, cache_timeout: 0.1, scope: $openc3_scope) - tlm(*args, type: :RAW, cache_timeout: cache_timeout, scope: scope) + def tlm_raw(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + tlm(*args, type: :RAW, cache_timeout: cache_timeout, scope: scope, token: token) end - def tlm_formatted(*args, cache_timeout: 0.1, scope: $openc3_scope) - tlm(*args, type: :FORMATTED, cache_timeout: cache_timeout, scope: scope) + def tlm_formatted(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + tlm(*args, type: :FORMATTED, cache_timeout: cache_timeout, scope: scope, token: token) end - def tlm_with_units(*args, cache_timeout: 0.1, scope: $openc3_scope) - tlm(*args, type: :WITH_UNITS, cache_timeout: cache_timeout, scope: scope) + def tlm_with_units(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + tlm(*args, type: :WITH_UNITS, cache_timeout: cache_timeout, scope: scope, token: token) end # @deprecated Use tlm with type: - def tlm_variable(*args, cache_timeout: 0.1, scope: $openc3_scope) - tlm(*args[0..-2], type: args[-1].intern, cache_timeout: cache_timeout, scope: scope) + def tlm_variable(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + tlm(*args[0..-2], type: args[-1].intern, cache_timeout: cache_timeout, scope: scope, token: token) end # Set a telemetry item in the current value table. @@ -104,9 +104,9 @@ def tlm_variable(*args, cache_timeout: 0.1, scope: $openc3_scope) # # @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) + 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) + 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 @@ -116,8 +116,8 @@ def set_tlm(*args, type: :CONVERTED, scope: $openc3_scope) # @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) - authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope) + 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 target_name = target_name.upcase packet_name = packet_name.upcase @@ -165,15 +165,15 @@ def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, scop # 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) + 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) + 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 # Get the list of CVT overrides - def get_overrides(scope: $openc3_scope) - authorize(permission: 'tlm', scope: scope) + def get_overrides(scope: $openc3_scope, token: $openc3_token) + authorize(permission: 'tlm', scope: scope, token: token) CvtModel.overrides(scope: scope) end @@ -190,9 +190,9 @@ def get_overrides(scope: $openc3_scope) # (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) + 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) + 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 @@ -201,10 +201,10 @@ def normalize_tlm(*args, type: :ALL, scope: $openc3_scope) # @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) + def get_tlm_buffer(target_name, packet_name, scope: $openc3_scope, token: $openc3_token) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) + 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) @@ -224,10 +224,10 @@ def get_tlm_buffer(target_name, packet_name, scope: $openc3_scope) # @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, cache_timeout: 0.1, scope: $openc3_scope) + def get_tlm_packet(target_name, packet_name, stale_time: 30, type: :CONVERTED, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) + 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? @@ -247,7 +247,7 @@ def get_tlm_packet(target_name, packet_name, stale_time: 30, type: :CONVERTED, c # @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, cache_timeout: 0.1, scope: $openc3_scope) + def get_tlm_values(items, stale_time: 30, cache_timeout: 0.1, 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 @@ -264,7 +264,7 @@ def get_tlm_values(items, stale_time: 30, cache_timeout: 0.1, scope: $openc3_sco end packets.uniq! packets.each do |target_name, packet_name| - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) end CvtModel.get_tlm_values(cvt_items, stale_time: stale_time, cache_timeout: cache_timeout, scope: scope) end @@ -274,9 +274,9 @@ def get_tlm_values(items, stale_time: 30, cache_timeout: 0.1, scope: $openc3_sco # @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) + def get_all_telemetry(target_name, scope: $openc3_scope, token: $openc3_token) target_name = target_name.upcase - authorize(permission: 'tlm', target_name: target_name, scope: scope) + authorize(permission: 'tlm', target_name: target_name, scope: scope, token: token) TargetModel.packets(target_name, type: :TLM, scope: scope) end @@ -285,9 +285,9 @@ def get_all_telemetry(target_name, scope: $openc3_scope) # @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) + def get_all_telemetry_names(target_name, scope: $openc3_scope, token: $openc3_token) target_name = target_name.upcase - authorize(permission: 'cmd_info', target_name: target_name, scope: scope) + authorize(permission: 'cmd_info', target_name: target_name, scope: scope, token: token) TargetModel.packet_names(target_name, type: :TLM, scope: scope) end @@ -297,10 +297,10 @@ def get_all_telemetry_names(target_name, scope: $openc3_scope) # @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) + def get_telemetry(target_name, packet_name, scope: $openc3_scope, token: $openc3_token) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) + authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) TargetModel.packet(target_name, packet_name, scope: scope) end @@ -311,11 +311,11 @@ def get_telemetry(target_name, packet_name, scope: $openc3_scope) # @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) + def get_item(target_name, packet_name, item_name, scope: $openc3_scope, token: $openc3_token) target_name = target_name.upcase packet_name = packet_name.upcase item_name = item_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) + 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 @@ -327,7 +327,7 @@ def get_item(target_name, packet_name, item_name, scope: $openc3_scope) # # @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) + 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 @@ -336,7 +336,7 @@ def subscribe_packets(packets, scope: $openc3_scope) packets.each do |target_name, packet_name| target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) + 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' @@ -351,8 +351,8 @@ def subscribe_packets(packets, scope: $openc3_scope) # @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) - authorize(permission: 'tlm', scope: scope) + 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 @@ -375,10 +375,10 @@ def get_packets(id, block: nil, count: 1000, scope: $openc3_scope) # @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) + def get_tlm_cnt(target_name, packet_name, scope: $openc3_scope, token: $openc3_token) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'system', target_name: target_name, packet_name: packet_name, scope: scope) + 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 @@ -387,8 +387,8 @@ def get_tlm_cnt(target_name, packet_name, scope: $openc3_scope) # # @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) - authorize(permission: 'system', scope: scope) + 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| target_name = target_name.upcase @@ -403,10 +403,10 @@ def get_tlm_cnts(target_packets, scope: $openc3_scope) # @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) + def get_packet_derived_items(target_name, packet_name, scope: $openc3_scope, token: $openc3_token) target_name = target_name.upcase packet_name = packet_name.upcase - authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope) + 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 @@ -427,7 +427,7 @@ def _validate_tlm_type(type) return nil end - def tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope) + def tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) case args.length when 1 target_name, packet_name, item_name = extract_fields_from_tlm_text(args[0]) @@ -453,7 +453,7 @@ def tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope return [target_name, packet_name, item_name] end - def set_tlm_process_args(args, method_name, scope: $openc3_scope) + def set_tlm_process_args(args, method_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]) From f7918d153daa817125f7fe39c20283108fe84d43 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Thu, 12 Oct 2023 20:22:48 -0600 Subject: [PATCH 03/17] interface and stash apis --- openc3/lib/openc3/models/cvt_model.rb | 3 +- openc3/lib/openc3/models/interface_model.rb | 8 +- openc3/python/openc3/api/interface_api.py | 227 ++++++++++-------- openc3/python/openc3/api/stash_api.py | 8 +- openc3/python/openc3/interfaces/interface.py | 2 +- .../microservices/decom_microservice.py | 3 +- .../microservices/interface_microservice.py | 32 +-- openc3/python/openc3/models/cvt_model.py | 4 +- .../python/openc3/models/interface_model.py | 79 ++++++ openc3/python/openc3/models/stash_model.py | 10 +- openc3/python/openc3/packets/telemetry.py | 4 +- openc3/python/test/api/test_interface_api.py | 223 +++++++++++++++++ openc3/python/test/api/test_stash_api.py | 72 ++++++ openc3/python/test/api/test_tlm_api.py | 4 +- openc3/python/test/models/test_cvt_model.py | 5 +- openc3/python/test/packets/test_packet.py | 2 +- openc3/spec/models/cvt_model_spec.rb | 2 + 17 files changed, 538 insertions(+), 150 deletions(-) create mode 100644 openc3/python/test/api/test_interface_api.py create mode 100644 openc3/python/test/api/test_stash_api.py diff --git a/openc3/lib/openc3/models/cvt_model.rb b/openc3/lib/openc3/models/cvt_model.rb index 4c9beb3c95..ff34e47b15 100644 --- a/openc3/lib/openc3/models/cvt_model.rb +++ b/openc3/lib/openc3/models/cvt_model.rb @@ -237,10 +237,11 @@ def self.normalize(target_name, packet_name, item_name, type: :ALL, scope: $open end tgt_pkt_key = "#{scope}__tlm__#{target_name}__#{packet_name}" - @@override_cache[tgt_pkt_key] = [Time.now, hash] if hash.empty? + @@override_cache.delete(tgt_pkt_key) Store.hdel("#{scope}__override__#{target_name}", packet_name) else + @@override_cache[tgt_pkt_key] = [Time.now, hash] Store.hset("#{scope}__override__#{target_name}", packet_name, JSON.generate(hash.as_json(:allow_nan => true))) end end diff --git a/openc3/lib/openc3/models/interface_model.rb b/openc3/lib/openc3/models/interface_model.rb index 4ad663b9d8..fc10ffe37d 100644 --- a/openc3/lib/openc3/models/interface_model.rb +++ b/openc3/lib/openc3/models/interface_model.rb @@ -423,7 +423,7 @@ def unmap_target(target_name, cmd_only: false, tlm_only: false) # Respawn the microservice type = self.class._get_type microservice_name = "#{@scope}__#{type}__#{@name}" - microservice = MicroserviceModel.get_model(name: microservice_name, scope: scope) + microservice = MicroserviceModel.get_model(name: microservice_name, scope: @scope) microservice.target_names.delete(target_name) unless @target_names.include?(target_name) microservice.update end @@ -438,11 +438,11 @@ def map_target(target_name, cmd_only: false, tlm_only: false, unmap_old: true) if unmap_old # Remove from old interface - all_interfaces = InterfaceModel.all(scope: scope) + all_interfaces = InterfaceModel.all(scope: @scope) old_interface = nil all_interfaces.each do |old_interface_name, old_interface_details| if old_interface_details['target_names'].include?(target_name) - old_interface = InterfaceModel.from_json(old_interface_details, scope: scope) + old_interface = InterfaceModel.from_json(old_interface_details, scope: @scope) old_interface.unmap_target(target_name, cmd_only: cmd_only, tlm_only: tlm_only) if old_interface end end @@ -457,7 +457,7 @@ def map_target(target_name, cmd_only: false, tlm_only: false, unmap_old: true) # Respawn the microservice type = self.class._get_type microservice_name = "#{@scope}__#{type}__#{@name}" - microservice = MicroserviceModel.get_model(name: microservice_name, scope: scope) + microservice = MicroserviceModel.get_model(name: microservice_name, scope: @scope) microservice.target_names << target_name unless microservice.target_names.include?(target_name) microservice.update end diff --git a/openc3/python/openc3/api/interface_api.py b/openc3/python/openc3/api/interface_api.py index 9cb7781544..8731d4d3c3 100644 --- a/openc3/python/openc3/api/interface_api.py +++ b/openc3/python/openc3/api/interface_api.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # @@ -20,13 +18,9 @@ from openc3.environment import OPENC3_SCOPE from openc3.utilities.authorization import authorize from openc3.models.interface_model import InterfaceModel - -# from openc3.utilities.logger import Logger - -# require 'openc3/models/interface_model' -# require 'openc3/models/interface_status_model' -# require 'openc3/topics/interface_topic' - +from openc3.models.interface_status_model import InterfaceStatusModel +from openc3.topics.interface_topic import InterfaceTopic +from openc3.utilities.logger import Logger WHITELIST.extend( [ @@ -54,102 +48,129 @@ def get_interface(interface_name, scope=OPENC3_SCOPE): interface = InterfaceModel.get(name=interface_name, scope=scope) if not interface: raise RuntimeError(f"Interface '{interface_name}' does not exist") - return interface - # interface.merge(InterfaceStatusModel.get(name=interface_name, scope=scope)) + return interface | InterfaceStatusModel.get(name=interface_name, scope=scope) # @return [Array] All the interface names def get_interface_names(scope=OPENC3_SCOPE): authorize(permission="system", scope=scope) - InterfaceModel.names(scope=scope) - - -# # Connects an interface and starts its telemetry gathering thread -# # -# # @param interface_name [String] The name of the interface -# # @param interface_params [Array] Optional parameters to pass to the interface -# def connect_interface(interface_name, *interface_params, scope=OPENC3_SCOPE): -# authorize(permission: 'system_set', interface_name: interface_name, scope: scope) -# InterfaceTopic.connect_interface(interface_name, *interface_params, scope: scope) - - -# # Disconnects from an interface and kills its telemetry gathering thread -# # -# # @param interface_name [String] The name of the interface -# def disconnect_interface(interface_name, scope=OPENC3_SCOPE): -# authorize(permission: 'system_set', interface_name: interface_name, scope: scope) -# InterfaceTopic.disconnect_interface(interface_name, scope: scope) - - -# # Starts raw logging for an interface -# # -# # @param interface_name [String] The name of the interface -# def start_raw_logging_interface(interface_name = 'ALL', scope=OPENC3_SCOPE): -# authorize(permission: 'system_set', interface_name: interface_name, scope: scope) -# if interface_name == 'ALL' -# get_interface_names().each do |interface_name| -# InterfaceTopic.start_raw_logging(interface_name, scope: scope) - -# else -# InterfaceTopic.start_raw_logging(interface_name, scope: scope) - - -# # Stop raw logging for an interface -# # -# # @param interface_name [String] The name of the interface -# def stop_raw_logging_interface(interface_name = 'ALL', scope=OPENC3_SCOPE): -# authorize(permission: 'system_set', interface_name: interface_name, scope: scope) -# if interface_name == 'ALL' -# get_interface_names().each do |interface_name| -# InterfaceTopic.stop_raw_logging(interface_name, scope: scope) - -# else -# InterfaceTopic.stop_raw_logging(interface_name, scope: scope) - - -# # Get information about all interfaces -# # -# # @return [Array>] Array of Arrays containing \[name, state, num clients, -# # TX queue size, RX queue size, TX bytes, RX bytes, Command count, -# # Telemetry count] for all interfaces -# def get_all_interface_info(scope=OPENC3_SCOPE): -# authorize(permission: 'system', scope: scope) -# info = [] -# InterfaceStatusModel.all(scope: scope).each do |int_name, int| -# info << [int['name'], int['state'], int['clients'], int['txsize'], int['rxsize'], -# int['txbytes'], int['rxbytes'], int['txcnt'], int['rxcnt']] - -# info.sort! { |a, b| a[0] <=> b[0] } -# info - - -# # Associates a target and all its commands and telemetry with a particular -# # interface. All the commands will go out over and telemetry be received -# # from that interface. -# # -# # @param target_name [String/Array] The name of the target(s) -# # @param interface_name (see #connect_interface) -# def map_target_to_interface(target_name, interface_name, cmd_only: false, tlm_only: false, unmap_old: true, scope=OPENC3_SCOPE): -# authorize(permission: 'system_set', interface_name: interface_name, scope: scope) -# new_interface = InterfaceModel.get_model(name: interface_name, scope: scope) -# if Array === target_name -# target_names = target_name -# else -# target_names = [target_name] - -# target_names.each do |name| -# new_interface.map_target(name, cmd_only: cmd_only, tlm_only: tlm_only, unmap_old: unmap_old) -# Logger.info("Target #{name} mapped to Interface #{interface_name}", scope: scope) - -# nil - - -# def interface_cmd(interface_name, cmd_name, *cmd_params, scope=OPENC3_SCOPE): -# authorize(permission: 'system_set', interface_name: interface_name, scope: scope) -# InterfaceTopic.interface_cmd(interface_name, cmd_name, *cmd_params, scope: scope) - - -# def interface_protocol_cmd(interface_name, cmd_name, *cmd_params, read_write: :READ_WRITE, index: -1, scope=OPENC3_SCOPE): -# authorize(permission: 'system_set', interface_name: interface_name, scope: scope) -# InterfaceTopic.protocol_cmd(interface_name, cmd_name, *cmd_params, read_write: read_write, index: index, scope: scope) + return InterfaceModel.names(scope=scope) + + +# Connects an interface and starts its telemetry gathering thread +# +# @param interface_name [String] The name of the interface +# @param interface_params [Array] Optional parameters to pass to the interface +def connect_interface(interface_name, *interface_params, scope=OPENC3_SCOPE): + authorize(permission="system_set", interface_name=interface_name, scope=scope) + InterfaceTopic.connect_interface(interface_name, *interface_params, scope=scope) + + +# Disconnects from an interface and kills its telemetry gathering thread +# +# @param interface_name [String] The name of the interface +def disconnect_interface(interface_name, scope=OPENC3_SCOPE): + authorize(permission="system_set", interface_name=interface_name, scope=scope) + InterfaceTopic.disconnect_interface(interface_name, scope=scope) + + +# Starts raw logging for an interface +# +# @param interface_name [String] The name of the interface +def start_raw_logging_interface(interface_name="ALL", scope=OPENC3_SCOPE): + authorize(permission="system_set", interface_name=interface_name, scope=scope) + if interface_name == "ALL": + for interface_name in get_interface_names(): + InterfaceTopic.start_raw_logging(interface_name, scope=scope) + else: + InterfaceTopic.start_raw_logging(interface_name, scope=scope) + + +# Stop raw logging for an interface +# +# @param interface_name [String] The name of the interface +def stop_raw_logging_interface(interface_name="ALL", scope=OPENC3_SCOPE): + authorize(permission="system_set", interface_name=interface_name, scope=scope) + if interface_name == "ALL": + for interface_name in get_interface_names(): + InterfaceTopic.stop_raw_logging(interface_name, scope=scope) + else: + InterfaceTopic.stop_raw_logging(interface_name, scope=scope) + + +# Get information about all interfaces +# +# @return [Array>] Array of Arrays containing \[name, state, num clients, +# TX queue size, RX queue size, TX bytes, RX bytes, Command count, +# Telemetry count] for all interfaces +def get_all_interface_info(scope=OPENC3_SCOPE): + authorize(permission="system", scope=scope) + info = [] + for int_name, int in InterfaceStatusModel.all(scope=scope).items(): + info.append( + [ + int["name"], + int["state"], + int["clients"], + int["txsize"], + int["rxsize"], + int["txbytes"], + int["rxbytes"], + int["txcnt"], + int["rxcnt"], + ] + ) + # info.sort! { |a, b| a[0] <: b[0] } + return info + + +# Associates a target and all its commands and telemetry with a particular +# interface. All the commands will go out over and telemetry be received +# from that interface. +# +# @param target_name [String/Array] The name of the target(s) +# @param interface_name (see #connect_interface) +def map_target_to_interface( + target_name, + interface_name, + cmd_only=False, + tlm_only=False, + unmap_old=True, + scope=OPENC3_SCOPE, +): + authorize(permission="system_set", interface_name=interface_name, scope=scope) + new_interface = InterfaceModel.get_model(name=interface_name, scope=scope) + if type(target_name) is list: + target_names = target_name + else: + target_names = [target_name] + for name in target_names: + new_interface.map_target( + name, cmd_only=cmd_only, tlm_only=tlm_only, unmap_old=unmap_old + ) + Logger.info(f"Target {name} mapped to Interface {interface_name}", scope=scope) + + +def interface_cmd(interface_name, cmd_name, *cmd_params, scope=OPENC3_SCOPE): + authorize(permission="system_set", interface_name=interface_name, scope=scope) + InterfaceTopic.interface_cmd(interface_name, cmd_name, *cmd_params, scope=scope) + + +def interface_protocol_cmd( + interface_name, + cmd_name, + *cmd_params, + read_write="READ_WRITE", + index=-1, + scope=OPENC3_SCOPE, +): + authorize(permission="system_set", interface_name=interface_name, scope=scope) + InterfaceTopic.protocol_cmd( + interface_name, + cmd_name, + *cmd_params, + read_write=read_write, + index=index, + scope=scope, + ) diff --git a/openc3/python/openc3/api/stash_api.py b/openc3/python/openc3/api/stash_api.py index b841985ff2..723bbeea77 100644 --- a/openc3/python/openc3/api/stash_api.py +++ b/openc3/python/openc3/api/stash_api.py @@ -25,9 +25,7 @@ def stash_set(key, value, scope=OPENC3_SCOPE): authorize(permission="script_run", scope=scope) - return StashModel.set( - {"name": key, "value": json.dumps(value.as_json())}, scope=scope - ) + return StashModel.set({"name": key, "value": json.dumps(value)}, scope=scope) def stash_get(key, scope=OPENC3_SCOPE): @@ -42,7 +40,7 @@ def stash_get(key, scope=OPENC3_SCOPE): def stash_all(scope=OPENC3_SCOPE): authorize(permission="script_view", scope=scope) all = StashModel.all(scope=scope) - for key, hash in all: + for key, hash in all.items(): all[key] = json.loads(hash["value"]) return all @@ -56,5 +54,5 @@ def stash_delete(key, scope=OPENC3_SCOPE): authorize(permission="script_run", scope=scope) model = StashModel.get_model(name=key, scope=scope) if model: - model.destroy + model.destroy() return model diff --git a/openc3/python/openc3/interfaces/interface.py b/openc3/python/openc3/interfaces/interface.py index d6869e39c1..db77db0a8e 100644 --- a/openc3/python/openc3/interfaces/interface.py +++ b/openc3/python/openc3/interfaces/interface.py @@ -143,7 +143,7 @@ def read(self): if not first or len(self.read_protocols) <= 0: # Read data for a packet data, extra = self.read_interface() - if not data: + if data is None: Logger.info(f"{self.name}: read_interface requested disconnect") return None else: diff --git a/openc3/python/openc3/microservices/decom_microservice.py b/openc3/python/openc3/microservices/decom_microservice.py index a47dce0ccf..fd9bf80b35 100644 --- a/openc3/python/openc3/microservices/decom_microservice.py +++ b/openc3/python/openc3/microservices/decom_microservice.py @@ -107,8 +107,7 @@ def decom_packet(self, topic, msg_id, msg_hash, _redis): packet.received_count = int(msg_hash[b"received_count"].decode()) extra = msg_hash.get(b"extra") if extra is not None: - extra = json.loads(extra.decode(), allow_nan=True, create_additions=True) - packet.extra = extra + packet.extra = json.loads(extra) packet.buffer = msg_hash[b"buffer"] packet.check_limits( System.limits_set() diff --git a/openc3/python/openc3/microservices/interface_microservice.py b/openc3/python/openc3/microservices/interface_microservice.py index 25319642e3..070420f214 100644 --- a/openc3/python/openc3/microservices/interface_microservice.py +++ b/openc3/python/openc3/microservices/interface_microservice.py @@ -74,6 +74,7 @@ def stop(self): def graceful_kill(self): InterfaceTopic.shutdown(self.interface, scope=self.scope) + time.sleep(0.001) # Allow other threads to run def run(self): # receive_commands does a while True and does not return @@ -133,20 +134,18 @@ def process_cmd(self, topic, msg_id, msg_hash, redis): else: return f"Interface not connected: {self.interface.name}" if msg_hash.get(b"log_stream"): - if msg_hash[b"log_stream"].decode() == "True": + if msg_hash[b"log_stream"].decode() == "true": self.logger.info(f"{self.interface.name}: Enable stream logging") - self.interface.start_raw_logging + self.interface.start_raw_logging() else: self.logger.info(f"{self.interface.name}: Disable stream logging") - self.interface.stop_raw_logging + self.interface.stop_raw_logging() return "SUCCESS" if msg_hash.get(b"interface_cmd"): - params = json.loads( - msg_hash[b"interface_cmd"], allow_nan=True, create_additions=True - ) + params = json.loads(msg_hash[b"interface_cmd"]) try: self.logger.info( - f"{self.interface.name}: interface_cmd= {params['cmd_name']} {' '.join(params['cmd_params'])}" + f"{self.interface.name}: interface_cmd: {params['cmd_name']} {' '.join(params['cmd_params'])}" ) self.interface.interface_cmd( params["cmd_name"], *params["cmd_params"] @@ -158,9 +157,7 @@ def process_cmd(self, topic, msg_id, msg_hash, redis): return error.message return "SUCCESS" if msg_hash.get(b"protocol_cmd"): - params = json.loads( - msg_hash[b"protocol_cmd"], allow_nan=True, create_additions=True - ) + params = json.loads(msg_hash[b"protocol_cmd"]) try: self.logger.info( f"{self.interface.name}: protocol_cmd: {params['cmd_name']} {' '.join(params['cmd_params'])} read_write: {params['read_write']} index: {params['index']}" @@ -285,6 +282,7 @@ def stop(self): def graceful_kill(self): RouterTopic.shutdown(self.router, scope=self.scope) + time.sleep(0.001) # Allow other threads to run def run(self): for topic, msg_id, msg_hash, redis in RouterTopic.receive_telemetry( @@ -324,16 +322,14 @@ def run(self): self.logger.info(f"{self.router.name}: Disconnect requested") self.tlm.disconnect(False) if msg_hash.get(b"log_stream"): - if msg_hash[b"log_stream"].decode() == "True": + if msg_hash[b"log_stream"].decode() == "true": self.logger.info(f"{self.router.name}: Enable stream logging") self.router.start_raw_logging else: self.logger.info(f"{self.router.name}: Disable stream logging") self.router.stop_raw_logging if msg_hash.get(b"router_cmd"): - params = json.loads( - msg_hash[b"router_cmd"], allow_nan=True, create_additions=True - ) + params = json.loads(msg_hash[b"router_cmd"]) try: self.logger.info( f"{self.router.name}: router_cmd: {params['cmd_name']} {' '.join(params['cmd_params'])}" @@ -348,9 +344,7 @@ def run(self): return error.message return "SUCCESS" if msg_hash.get(b"protocol_cmd"): - params = json.loads( - msg_hash[b"protocol_cmd"], allow_nan=True, create_additions=True - ) + params = json.loads(msg_hash[b"protocol_cmd"]) try: self.logger.info( f"{self.router.name}: protocol_cmd: {params['cmd_name']} {' '.join(params['cmd_params'])} read_write: {params['read_write']} index: {params['index']}" @@ -481,7 +475,7 @@ def attempting(self, *params): if len(params) != 0: self.interface.disconnect() # Build New Interface, this can fail if passed bad parameters - new_interface = self.interface.__class__.__name__(*params) + new_interface = self.interface(*params) self.interface.copy_to(new_interface) # Replace interface for targets @@ -540,7 +534,7 @@ def run(self): if self.interface.read_allowed: try: packet = self.interface.read() - if packet: + if packet is not None: self.handle_packet(packet) self.count += 1 if self.interface_or_router == "INTERFACE": diff --git a/openc3/python/openc3/models/cvt_model.py b/openc3/python/openc3/models/cvt_model.py index ebe1f1ba76..505cdcc00e 100644 --- a/openc3/python/openc3/models/cvt_model.py +++ b/openc3/python/openc3/models/cvt_model.py @@ -295,10 +295,12 @@ def normalize( f"Unknown type '{type}' for {target_name} {packet_name} {item_name}" ) tgt_pkt_key = f"{scope}__tlm__{target_name}__{packet_name}" - CvtModel.override_cache[tgt_pkt_key] = [time.time(), hash] if len(hash) == 0: + if tgt_pkt_key in CvtModel.override_cache: + CvtModel.override_cache.pop(tgt_pkt_key) Store.hdel(f"{scope}__override__{target_name}", packet_name) else: + CvtModel.override_cache[tgt_pkt_key] = [time.time(), hash] Store.hset( f"{scope}__override__{target_name}", packet_name, json.dumps(hash) ) diff --git a/openc3/python/openc3/models/interface_model.py b/openc3/python/openc3/models/interface_model.py index 50d6882383..d086a6a8f3 100644 --- a/openc3/python/openc3/models/interface_model.py +++ b/openc3/python/openc3/models/interface_model.py @@ -16,6 +16,8 @@ import os from openc3.models.model import Model +from openc3.models.target_model import TargetModel +from openc3.models.microservice_model import MicroserviceModel from openc3.logs.stream_log_pair import StreamLogPair from openc3.top_level import get_class_from_module from openc3.utilities.string import filename_to_module, filename_to_class_name @@ -209,3 +211,80 @@ def as_json(self): "prefix": self.prefix, "updated_at": self.updated_at, } + + def ensure_target_exists(self, target_name): + target = TargetModel.get(name=target_name, scope=self.scope) + if not target: + raise RuntimeError(f"Target {target_name} does not exist") + return target + + def unmap_target(self, target_name, cmd_only=False, tlm_only=False): + if cmd_only and tlm_only: + cmd_only = False + tlm_only = False + target_name = str(target_name).upper() + + # Remove from this interface + if cmd_only: + self.cmd_target_names.remove(target_name) + if target_name not in self.tlm_target_names: + self.target_names.remove(target_name) + elif tlm_only: + self.tlm_target_names.remove(target_name) + if target_name not in self.cmd_target_names: + self.target_names.remove(target_name) + else: + self.cmd_target_names.remove(target_name) + self.tlm_target_names.remove(target_name) + self.target_names.remove(target_name) + self.update() + + # Respawn the microservice + type = self.__class__.__name__.split("Model")[0].upper() + microservice_name = f"{self.scope}__{type}__{self.name}" + microservice = MicroserviceModel.get_model( + name=microservice_name, scope=self.scope + ) + if target_name not in self.target_names: + microservice.target_names.remove(target_name) + microservice.update() + + def map_target(self, target_name, cmd_only=False, tlm_only=False, unmap_old=True): + if cmd_only and tlm_only: + cmd_only = False + tlm_only = False + target_name = str(target_name).upper() + self.ensure_target_exists(target_name) + + if unmap_old: + # Remove from old interface + all_interfaces = InterfaceModel.all(scope=self.scope) + old_interface = None + for _, old_interface_details in all_interfaces.items(): + if target_name in old_interface_details["target_names"]: + old_interface = InterfaceModel.from_json( + old_interface_details, scope=self.scope + ) + if old_interface: + old_interface.unmap_target( + target_name, cmd_only=cmd_only, tlm_only=tlm_only + ) + + # Add to this interface + if target_name not in self.target_names: + self.target_names.append(target_name) + if target_name not in self.cmd_target_names or tlm_only: + self.cmd_target_names.append(target_name) + if target_name not in self.tlm_target_names or cmd_only: + self.tlm_target_names.append(target_name) + self.update() + + # Respawn the microservice + type = self.__class__.__name__.split("Model")[0].upper() + microservice_name = f"{self.scope}__{type}__{self.name}" + microservice = MicroserviceModel.get_model( + name=microservice_name, scope=self.scope + ) + if target_name not in microservice.target_names: + microservice.target_names.append(target_name) + microservice.update() diff --git a/openc3/python/openc3/models/stash_model.py b/openc3/python/openc3/models/stash_model.py index 267fad9d0b..74055a7d6d 100644 --- a/openc3/python/openc3/models/stash_model.py +++ b/openc3/python/openc3/models/stash_model.py @@ -25,25 +25,25 @@ class StashModel(Model): # and are reimplemented to enable various Model class methods to work @classmethod def get(cls, name, scope=OPENC3_SCOPE): - super().get(f"{scope}__{StashModel.PRIMARY_KEY}", name=name) + return super().get(f"{scope}__{StashModel.PRIMARY_KEY}", name=name) @classmethod def names(cls, scope=OPENC3_SCOPE): - super().names(f"{scope}__{StashModel.PRIMARY_KEY}") + return super().names(f"{scope}__{StashModel.PRIMARY_KEY}") @classmethod def all(cls, scope=OPENC3_SCOPE): - super().all(f"{scope}__{StashModel.PRIMARY_KEY}") + return super().all(f"{scope}__{StashModel.PRIMARY_KEY}") # END NOTE def __init__(self, name, value, scope=OPENC3_SCOPE): - super.__init__(f"{scope}__{StashModel.PRIMARY_KEY}", name=name, scope=scope) + super().__init__(f"{scope}__{StashModel.PRIMARY_KEY}", name=name, scope=scope) self.value = value # self.return [Hash] JSON encoding of this model def as_json(self): return { "name": self.name, - "value": self.value.as_json(), + "value": self.value, } diff --git a/openc3/python/openc3/packets/telemetry.py b/openc3/python/openc3/packets/telemetry.py index 212576ced5..9914bda8cb 100644 --- a/openc3/python/openc3/packets/telemetry.py +++ b/openc3/python/openc3/packets/telemetry.py @@ -256,8 +256,8 @@ def packet(self, target_name, packet_name): # default value of nil means to search all known targets. # @return [Packet] The identified packet, Returns nil if no packet could be identified. def identify(self, packet_data, target_names=None): - if not target_names: - target_names = target_names() + if target_names is None: + target_names = self.target_names() for target_name in target_names: target_name = str(target_name).upper() diff --git a/openc3/python/test/api/test_interface_api.py b/openc3/python/test/api/test_interface_api.py new file mode 100644 index 0000000000..035da624f0 --- /dev/null +++ b/openc3/python/test/api/test_interface_api.py @@ -0,0 +1,223 @@ +# Copyright 2023 OpenC3, Inc. +# 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. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +import time +import threading +import unittest +from unittest.mock import * +from test.test_helper import * +from openc3.api.interface_api import * +from openc3.interfaces.interface import Interface +from openc3.models.target_model import TargetModel +from openc3.models.interface_model import InterfaceModel +from openc3.models.microservice_model import MicroserviceModel +from openc3.microservices.interface_microservice import InterfaceMicroservice + + +class TestInterfaceApi(unittest.TestCase): + interface_cmd_data = {} + protocol_cmd_data = {} + + @patch("openc3.models.interface_model.InterfaceModel.get_model") + @patch("openc3.microservices.interface_microservice.System") + def setUp(self, mock_system, mock_get_model): + mock_redis(self) + setup_system() + + class MyInterface(Interface): + target_names = ["INST"] + + def connected(self): + return True + + def disconnect(self): + pass + + def read_interface(self): + time.sleep(0.05) + return b"", "" + + def interface_cmd(self, cmd_name, *cmd_params): + TestInterfaceApi.interface_cmd_data[cmd_name] = cmd_params + + def protocol_cmd( + self, cmd_name, *cmd_params, read_write="READ_WRITE", index=-1 + ): + TestInterfaceApi.protocol_cmd_data[cmd_name] = cmd_params + + # Allow the stubbed InterfaceModel.get_model to call build() + def build(): + return MyInterface() + + mock_get_model.return_value = MyInterface + + model = InterfaceModel( + name="INST_INT", + scope="DEFAULT", + target_names=["INST"], + cmd_target_names=["INST"], + tlm_target_names=["INST"], + config_params=["openc3/interfaces/interface.py"], + ) + model.create() + model = MicroserviceModel( + name="DEFAULT__INTERFACE__INST_INT", scope="DEFAULT", target_names=["INST"] + ) + model.create + self.im = InterfaceMicroservice("DEFAULT__INTERFACE__INST_INT") + self.im_thread = threading.Thread(target=self.im.run) + self.im_thread.start() + time.sleep(0.001) # Allow the thread to run + + def tearDown(self): + self.im.shutdown() + time.sleep(0.001) + + def test_returns_interface_hash(self): + interface = get_interface("INST_INT") + self.assertEqual(type(interface), dict) + self.assertEqual(interface["name"], "INST_INT") + # Verify it also includes the status + self.assertEqual(interface["state"], "CONNECTED") + self.assertEqual(interface["clients"], 0) + + def test_returns_all_interface_names(self): + model = InterfaceModel(name="INT1", scope="DEFAULT") + model.create() + model = InterfaceModel(name="INT2", scope="DEFAULT") + model.create() + self.assertEqual(get_interface_names(), ["INST_INT", "INT1", "INT2"]) + + def test_connects_the_interface(self): + self.assertEqual(get_interface("INST_INT")["state"], "CONNECTED") + disconnect_interface("INST_INT") + time.sleep(0.1) + self.assertEqual(get_interface("INST_INT")["state"], "DISCONNECTED") + connect_interface("INST_INT") + time.sleep(0.1) + self.assertIn(get_interface("INST_INT")["state"], ["ATTEMPTING", "CONNECTED"]) + + def test_should_start_and_stop_raw_logging_on_the_interface(self): + self.assertIsNone(self.im.interface.stream_log_pair) + start_raw_logging_interface("INST_INT") + time.sleep(0.1) + self.assertTrue(self.im.interface.stream_log_pair.read_log.logging_enabled) + self.assertTrue(self.im.interface.stream_log_pair.write_log.logging_enabled) + stop_raw_logging_interface("INST_INT") + time.sleep(0.1) + self.assertFalse(self.im.interface.stream_log_pair.read_log.logging_enabled) + self.assertFalse(self.im.interface.stream_log_pair.write_log.logging_enabled) + + start_raw_logging_interface("ALL") + time.sleep(0.1) + self.assertTrue(self.im.interface.stream_log_pair.read_log.logging_enabled) + self.assertTrue(self.im.interface.stream_log_pair.write_log.logging_enabled) + stop_raw_logging_interface("ALL") + time.sleep(0.1) + self.assertFalse(self.im.interface.stream_log_pair.read_log.logging_enabled) + self.assertFalse(self.im.interface.stream_log_pair.write_log.logging_enabled) + # TODO: Need to explicitly shutdown stream_log_pair once started + self.im.interface.stream_log_pair.shutdown() + + def test_gets_interface_name_and_all_info(self): + info = get_all_interface_info() + self.assertEqual(info[0][0], "INST_INT") + self.assertEqual(info[0][1], "CONNECTED") + + def test_successfully_maps_a_target_to_an_interface(self): + TargetModel(name="INST", scope="DEFAULT").create() + TargetModel(name="INST2", scope="DEFAULT").create() + + model = MicroserviceModel( + name="DEFAULT__INTERFACE__INST_INT", + scope="DEFAULT", + target_names=["INST"], + ) + model.create() + model = MicroserviceModel( + name="DEFAULT__INTERFACE__INST2_INT", + scope="DEFAULT", + target_names=["INST2"], + ) + model.create() + + model2 = InterfaceModel( + name="INST2_INT", + scope="DEFAULT", + target_names=["INST2"], + cmd_target_names=["INST2"], + tlm_target_names=["INST2"], + config_params=["openc3/interfaces/interface.py"], + ) + model2.create() + self.assertEqual(model2.target_names, ["INST2"]) + + map_target_to_interface("INST2", "INST_INT") + + model1 = InterfaceModel.get_model(name="INST_INT", scope="DEFAULT") + model2 = InterfaceModel.get_model(name="INST2_INT", scope="DEFAULT") + self.assertEqual(model1.target_names, ["INST", "INST2"]) + self.assertEqual(model2.target_names, []) + + def test_sends_an_interface_cmd(self): + TestInterfaceApi.interface_cmd_data = {} + interface_cmd("INST_INT", "cmd1") + time.sleep(0.1) + self.assertEqual(list(TestInterfaceApi.interface_cmd_data.keys()), ["cmd1"]) + self.assertEqual(TestInterfaceApi.interface_cmd_data["cmd1"], ()) + + TestInterfaceApi.interface_cmd_data = {} + interface_cmd("INST_INT", "cmd2", "param1") + time.sleep(0.1) + self.assertEqual(list(TestInterfaceApi.interface_cmd_data.keys()), ["cmd2"]) + self.assertEqual(TestInterfaceApi.interface_cmd_data["cmd2"], ("param1",)) + + TestInterfaceApi.interface_cmd_data = {} + interface_cmd("INST_INT", "cmd3", "param1", "param2") + time.sleep(0.1) + self.assertEqual(list(TestInterfaceApi.interface_cmd_data.keys()), ["cmd3"]) + self.assertEqual( + TestInterfaceApi.interface_cmd_data["cmd3"], + ( + "param1", + "param2", + ), + ) + + def test_sends_a_protocol_cmd(self): + TestInterfaceApi.protocol_cmd_data = {} + interface_protocol_cmd("INST_INT", "cmd1") + time.sleep(0.1) + self.assertEqual(list(TestInterfaceApi.protocol_cmd_data.keys()), ["cmd1"]) + self.assertEqual(TestInterfaceApi.protocol_cmd_data["cmd1"], ()) + + TestInterfaceApi.protocol_cmd_data = {} + interface_protocol_cmd("INST_INT", "cmd2", "param1") + time.sleep(0.1) + self.assertEqual(list(TestInterfaceApi.protocol_cmd_data.keys()), ["cmd2"]) + self.assertEqual(TestInterfaceApi.protocol_cmd_data["cmd2"], ("param1",)) + + TestInterfaceApi.protocol_cmd_data = {} + interface_protocol_cmd("INST_INT", "cmd3", "param1", "param2") + time.sleep(0.1) + self.assertEqual(list(TestInterfaceApi.protocol_cmd_data.keys()), ["cmd3"]) + self.assertEqual( + TestInterfaceApi.protocol_cmd_data["cmd3"], + ( + "param1", + "param2", + ), + ) diff --git a/openc3/python/test/api/test_stash_api.py b/openc3/python/test/api/test_stash_api.py new file mode 100644 index 0000000000..09f01adceb --- /dev/null +++ b/openc3/python/test/api/test_stash_api.py @@ -0,0 +1,72 @@ +# Copyright 2023 OpenC3, Inc. +# 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. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +import unittest +from unittest.mock import * +from test.test_helper import * +from openc3.api.stash_api import * + + +class TestStashApi(unittest.TestCase): + def setUp(self): + mock_redis(self) + + def test_sets_a_value_in_the_stash(self): + stash_set("key", "val") + self.assertEqual(stash_get("key"), "val") + # Override with binary data + stash_set("key", "\xDE\xAD\xBE\xEF") + self.assertEqual(stash_get("key"), "\xDE\xAD\xBE\xEF") + + def test_sets_an_array_in_the_stash(self): + data = [1, 2, [3, 4]] + stash_set("key", data) + self.assertEqual(stash_get("key"), data) + + def test_sets_a_hash_in_the_stash(self): + data = {"key": "val", "more": 1} + stash_set("key", data) + self.assertEqual(stash_get("key"), ({"key": "val", "more": 1})) + + def test_returns_None_if_the_value_doesnt_exist(self): + self.assertIsNone(stash_get("nope")) + + def test_deletes_an_existing_key(self): + stash_set("key", "val") + stash_delete("key") + self.assertIsNone(stash_get("key")) + + def test_ignores_keys_that_do_not_exist(self): + stash_delete("nope") + + def test_returns_empty_array_with_no_keys(self): + self.assertEqual(stash_keys(), ([])) + + def test_returns_all_the_stash_keys_as_an_array(self): + stash_set("key1", "val") + stash_set("key2", "val") + stash_set("key3", "val") + self.assertEqual(stash_keys(), ["key1", "key2", "key3"]) + + def test_returns_empty_hash_with_no_keys(self): + self.assertEqual(stash_all(), ({})) + + def test_returns_all_stash_values_as_a_hash(self): + stash_set("key1", 1) + stash_set("key2", 2) + stash_set("key3", 3) + result = {"key1": 1, "key2": 2, "key3": 3} + self.assertEqual(stash_all(), result) diff --git a/openc3/python/test/api/test_tlm_api.py b/openc3/python/test/api/test_tlm_api.py index 4156a24be6..e6331e5947 100644 --- a/openc3/python/test/api/test_tlm_api.py +++ b/openc3/python/test/api/test_tlm_api.py @@ -229,7 +229,7 @@ def test_inject_tlm_complains_about_non_existant_packets(self): def test_inject_tlm_complains_about_non_existant_items(self): with self.assertRaisesRegex( - RuntimeError, "Item\(s\) 'INST HEALTH_STATUS BLAH' does not exist" + RuntimeError, r"Item\(s\) 'INST HEALTH_STATUS BLAH' does not exist" ): inject_tlm("INST", "HEALTH_STATUS", {"BLAH": 0}) @@ -773,8 +773,6 @@ def test_get_tlm_values_complains_about_non_existant_value_types(self): get_tlm_values(["INST__HEALTH_STATUS__TEMP1__MINE"]) def test_get_tlm_values_complains_about_bad_arguments(self): - with self.assertRaises(TypeError): - get_tlm_values() with self.assertRaisesRegex(AttributeError, "items must be array of strings"): get_tlm_values([]) with self.assertRaisesRegex(AttributeError, "items must be array of strings"): diff --git a/openc3/python/test/models/test_cvt_model.py b/openc3/python/test/models/test_cvt_model.py index 99178902b8..2a1c734c86 100644 --- a/openc3/python/test/models/test_cvt_model.py +++ b/openc3/python/test/models/test_cvt_model.py @@ -41,7 +41,6 @@ def update_temp1(self, rxtime=None): if rxtime is None: rxtime = time.time() json_hash["RECEIVED_TIMESECONDS"] = rxtime - print(f"update_temp set RECEIVED_TIMESECONDS to {rxtime}") CvtModel.set( json_hash, target_name="INST", packet_name="HEALTH_STATUS", scope="DEFAULT" ) @@ -121,11 +120,11 @@ def test_decoms_and_sets(self): def test_deletes_a_target_packet_from_the_cvt(self): self.update_temp1() - self.assertEqual(Store.hkeys("DEFAULT__tlm__INST"), [b"HEALTH_STATUS"]) + self.assertIn(b"HEALTH_STATUS", Store.hkeys("DEFAULT__tlm__INST")) CvtModel.delete( target_name="INST", packet_name="HEALTH_STATUS", scope="DEFAULT" ) - self.assertEqual(Store.hkeys("DEFAULT__tlm__INST"), []) + self.assertNotIn(b"HEALTH_STATUS", Store.hkeys("DEFAULT__tlm__INST")) def test_raises_for_an_unknown_type(self): self.update_temp1() diff --git a/openc3/python/test/packets/test_packet.py b/openc3/python/test/packets/test_packet.py index e0a5c1b9fa..d35b4316b7 100644 --- a/openc3/python/test/packets/test_packet.py +++ b/openc3/python/test/packets/test_packet.py @@ -697,7 +697,7 @@ def test_clears_the_read_cache(self): self.p.buffer = b"\x04" cache = self.p.read_conversion_cache i.read_conversion = GenericConversion("value / 2") - self.assertIsNone(cache) + self.assertEqual(cache, {}) self.assertEqual(self.p.read("ITEM"), 2) cache = self.p.read_conversion_cache self.assertEqual(cache[i.name], 2) diff --git a/openc3/spec/models/cvt_model_spec.rb b/openc3/spec/models/cvt_model_spec.rb index 24f1177eea..4c691de927 100644 --- a/openc3/spec/models/cvt_model_spec.rb +++ b/openc3/spec/models/cvt_model_spec.rb @@ -275,7 +275,9 @@ def check_temp1 it "does nothing if no value overriden" do update_temp1() + cache_copy = CvtModel.class_variable_get(:@@override_cache).dup CvtModel.normalize("INST", "HEALTH_STATUS", "TEMP1", type: :RAW, scope: "DEFAULT") + expect(cache_copy).to eql CvtModel.class_variable_get(:@@override_cache) check_temp1() end From 982939f3561c8af04e10e9303dbda1bfd92f2131 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Fri, 13 Oct 2023 11:32:31 -0600 Subject: [PATCH 04/17] Minor testing updates --- openc3/lib/openc3/api/tlm_api.rb | 14 ++++----- openc3/lib/openc3/script/script.rb | 8 +++++ openc3/python/test/api/test_interface_api.py | 5 +-- openc3/spec/api/interface_api_spec.rb | 32 +++++++++++++++----- openc3/spec/api/router_api_spec.rb | 29 ++++++++++++++---- 5 files changed, 63 insertions(+), 25 deletions(-) diff --git a/openc3/lib/openc3/api/tlm_api.rb b/openc3/lib/openc3/api/tlm_api.rb index 6dd564532a..fdc9316f65 100644 --- a/openc3/lib/openc3/api/tlm_api.rb +++ b/openc3/lib/openc3/api/tlm_api.rb @@ -67,7 +67,7 @@ module Api # @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, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) - target_name, packet_name, item_name = tlm_process_args(args, 'tlm', cache_timeout: cache_timeout, scope: scope) + target_name, packet_name, item_name = _tlm_process_args(args, 'tlm', cache_timeout: cache_timeout, 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, cache_timeout: cache_timeout, scope: scope) end @@ -105,7 +105,7 @@ def tlm_variable(*args, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3 # @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) + 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 @@ -166,7 +166,7 @@ def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, scop # 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) + 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 @@ -191,7 +191,7 @@ def get_overrides(scope: $openc3_scope, token: $openc3_token) # @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) + 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 @@ -386,7 +386,7 @@ def get_tlm_cnt(target_name, packet_name, scope: $openc3_scope, token: $openc3_t # 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 + # @return [Array] Receive count for the telemetry packets def get_tlm_cnts(target_packets, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'system', scope: scope, token: token) counts = [] @@ -427,7 +427,7 @@ def _validate_tlm_type(type) return nil end - def tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) + def _tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope, token: $openc3_token) case args.length when 1 target_name, packet_name, item_name = extract_fields_from_tlm_text(args[0]) @@ -453,7 +453,7 @@ def tlm_process_args(args, method_name, cache_timeout: 0.1, scope: $openc3_scope return [target_name, packet_name, item_name] end - def set_tlm_process_args(args, method_name, scope: $openc3_scope, token: $openc3_token) + def _set_tlm_process_args(args, method_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]) diff --git a/openc3/lib/openc3/script/script.rb b/openc3/lib/openc3/script/script.rb index 97392d2432..14f6ac18c4 100644 --- a/openc3/lib/openc3/script/script.rb +++ b/openc3/lib/openc3/script/script.rb @@ -96,6 +96,10 @@ def disconnect_script $disconnect = true end + ########################################################################### + # START PUBLIC API + ########################################################################### + # DEPRECATED def play_wav_file(wav_filename) # NOOP @@ -202,6 +206,10 @@ def step_mode def run_mode # NOOP end + + ########################################################################### + # END PUBLIC API + ########################################################################### end # Provides a proxy to the JsonDRbObject which communicates with the API server diff --git a/openc3/python/test/api/test_interface_api.py b/openc3/python/test/api/test_interface_api.py index 035da624f0..d90447a9d0 100644 --- a/openc3/python/test/api/test_interface_api.py +++ b/openc3/python/test/api/test_interface_api.py @@ -59,6 +59,7 @@ def protocol_cmd( TestInterfaceApi.protocol_cmd_data[cmd_name] = cmd_params # Allow the stubbed InterfaceModel.get_model to call build() + @staticmethod def build(): return MyInterface() @@ -73,10 +74,6 @@ def build(): config_params=["openc3/interfaces/interface.py"], ) model.create() - model = MicroserviceModel( - name="DEFAULT__INTERFACE__INST_INT", scope="DEFAULT", target_names=["INST"] - ) - model.create self.im = InterfaceMicroservice("DEFAULT__INTERFACE__INST_INT") self.im_thread = threading.Thread(target=self.im.run) self.im_thread.start() diff --git a/openc3/spec/api/interface_api_spec.rb b/openc3/spec/api/interface_api_spec.rb index f71af79660..6682c3983c 100644 --- a/openc3/spec/api/interface_api_spec.rb +++ b/openc3/spec/api/interface_api_spec.rb @@ -54,7 +54,7 @@ class ApiTest model.create @im = InterfaceMicroservice.new("DEFAULT__INTERFACE__INST_INT") @im_thread = Thread.new { @im.run } - sleep(1) # Allow the thread to run + sleep(0.01) # Allow the thread to run @api = ApiTest.new end @@ -62,7 +62,7 @@ class ApiTest after(:each) do @im_shutdown = true @im.shutdown - sleep(0.1) + sleep(0.01) end describe "get_interface" do @@ -90,10 +90,10 @@ class ApiTest it "connects the interface" do expect(@api.get_interface("INST_INT")['state']).to eql "CONNECTED" @api.disconnect_interface("INST_INT") - sleep(2) + sleep(0.1) expect(@api.get_interface("INST_INT")['state']).to eql "DISCONNECTED" @api.connect_interface("INST_INT") - sleep(2) + sleep(0.1) expect(@api.get_interface("INST_INT")['state']).to eql "ATTEMPTING" end end @@ -102,13 +102,11 @@ class ApiTest it "should start raw logging on the interface" do expect_any_instance_of(OpenC3::Interface).to receive(:start_raw_logging) @api.start_raw_logging_interface("INST_INT") - sleep(0.1) end it "should start raw logging on all interfaces" do expect_any_instance_of(OpenC3::Interface).to receive(:start_raw_logging) @api.start_raw_logging_interface("ALL") - sleep(0.1) end end @@ -116,13 +114,11 @@ class ApiTest it "should stop raw logging on the interface" do expect_any_instance_of(OpenC3::Interface).to receive(:stop_raw_logging) @api.stop_raw_logging_interface("INST_INT") - sleep(0.1) end it "should stop raw logging on all interfaces" do expect_any_instance_of(OpenC3::Interface).to receive(:stop_raw_logging) @api.stop_raw_logging_interface("ALL") - sleep(0.1) end end @@ -156,5 +152,25 @@ class ApiTest expect(model2.target_names).to eq [] end end + + describe "interface_cmd" do + it "sends a comamnd to an interface" do + expect_any_instance_of(OpenC3::Interface).to receive(:interface_cmd).with("cmd1") + @api.interface_cmd("INST_INT", "cmd1") + + expect_any_instance_of(OpenC3::Interface).to receive(:interface_cmd).with("cmd1", "param1") + @api.interface_cmd("INST_INT", "cmd1", "param1") + end + end + + describe "interface_protocol_cmd" do + it "sends a comamnd to an interface" do + expect_any_instance_of(OpenC3::Interface).to receive(:protocol_cmd).with("cmd1", {index: -1, read_write: "READ_WRITE"}) + @api.interface_protocol_cmd("INST_INT", "cmd1") + + expect_any_instance_of(OpenC3::Interface).to receive(:protocol_cmd).with("cmd1", "param1", {index: -1, read_write: "READ_WRITE"}) + @api.interface_protocol_cmd("INST_INT", "cmd1", "param1") + end + end end end diff --git a/openc3/spec/api/router_api_spec.rb b/openc3/spec/api/router_api_spec.rb index 3ecee181d5..1e1d13059e 100644 --- a/openc3/spec/api/router_api_spec.rb +++ b/openc3/spec/api/router_api_spec.rb @@ -54,7 +54,7 @@ class ApiTest model.create @im = RouterMicroservice.new("DEFAULT__INTERFACE__ROUTE_INT") @im_thread = Thread.new { @im.run } - sleep(1) # Allow the thread to run + sleep(0.01) # Allow the thread to run @api = ApiTest.new end @@ -62,7 +62,7 @@ class ApiTest after(:each) do @im_shutdown = true @im.shutdown - sleep(0.1) + sleep(0.01) end describe "get_router" do @@ -102,13 +102,11 @@ class ApiTest it "should start raw logging on the router" do expect_any_instance_of(OpenC3::Interface).to receive(:start_raw_logging) @api.start_raw_logging_router("ROUTE_INT") - sleep(0.1) end it "should start raw logging on all routers" do expect_any_instance_of(OpenC3::Interface).to receive(:start_raw_logging) @api.start_raw_logging_router("ALL") - sleep(0.1) end end @@ -116,13 +114,11 @@ class ApiTest it "should stop raw logging on the router" do expect_any_instance_of(OpenC3::Interface).to receive(:stop_raw_logging) @api.stop_raw_logging_router("ROUTE_INT") - sleep(0.1) end it "should stop raw logging on all routers" do expect_any_instance_of(OpenC3::Interface).to receive(:stop_raw_logging) @api.stop_raw_logging_router("ALL") - sleep(0.1) end end @@ -132,5 +128,26 @@ class ApiTest expect(info[0][0]).to eq "ROUTE_INT" end end + + describe "router_cmd" do + it "sends a comamnd to an router_cmd" do + # Ultimately the router_cmd is still routed to interface_cmd on the interface + expect_any_instance_of(OpenC3::Interface).to receive(:interface_cmd).with("cmd1") + @api.router_cmd("ROUTE_INT", "cmd1") + + expect_any_instance_of(OpenC3::Interface).to receive(:interface_cmd).with("cmd1", "param1") + @api.router_cmd("ROUTE_INT", "cmd1", "param1") + end + end + + describe "router_protocol_cmd" do + it "sends a comamnd to an interface" do + expect_any_instance_of(OpenC3::Interface).to receive(:protocol_cmd).with("cmd1", {index: -1, read_write: "READ_WRITE"}) + @api.router_protocol_cmd("ROUTE_INT", "cmd1") + + expect_any_instance_of(OpenC3::Interface).to receive(:protocol_cmd).with("cmd1", "param1", {index: -1, read_write: "READ_WRITE"}) + @api.router_protocol_cmd("ROUTE_INT", "cmd1", "param1") + end + end end end From c4c5d918f98ceac739a7b164c35dbbe244a41914 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Sun, 15 Oct 2023 18:29:44 -0600 Subject: [PATCH 05/17] Cmd_api fixes --- openc3/lib/openc3/api/cmd_api.rb | 13 +- openc3/lib/openc3/script/script.rb | 50 ++++---- openc3/python/openc3/api/cmd_api.py | 43 ++++--- openc3/python/openc3/script/__init__.py | 13 +- openc3/python/openc3/script/api_shared.py | 8 +- openc3/python/openc3/script/commands.py | 8 +- openc3/python/openc3/script/cosmos_api.py | 133 -------------------- openc3/python/openc3/script/internal_api.py | 56 --------- openc3/python/test/api/test_cmd_api.py | 6 +- 9 files changed, 77 insertions(+), 253 deletions(-) delete mode 100644 openc3/python/openc3/script/cosmos_api.py delete mode 100644 openc3/python/openc3/script/internal_api.py diff --git a/openc3/lib/openc3/api/cmd_api.rb b/openc3/lib/openc3/api/cmd_api.rb index 787876bb8f..833b203091 100644 --- a/openc3/lib/openc3/api/cmd_api.rb +++ b/openc3/lib/openc3/api/cmd_api.rb @@ -285,7 +285,7 @@ def get_cmd_time(target_name = nil, command_name = nil, scope: $openc3_scope, to target_name = target_name.upcase command_name = command_name.upcase time = CommandDecomTopic.get_cmd_item(target_name, command_name, 'RECEIVED_TIMESECONDS', type: :CONVERTED, scope: scope) - [target_name, command_name, time.to_i, ((time.to_f - time.to_i) * 1_000_000).to_i] + return [target_name, command_name, time.to_i, ((time.to_f - time.to_i) * 1_000_000).to_i] else if target_name.nil? targets = TargetModel.names(scope: scope) @@ -293,9 +293,9 @@ def get_cmd_time(target_name = nil, command_name = nil, scope: $openc3_scope, to target_name = target_name.upcase targets = [target_name] end + time = 0 + command_name = nil targets.each do |target_name| - time = 0 - command_name = nil TargetModel.packets(target_name, type: :CMD, scope: scope).each do |packet| cur_time = CommandDecomTopic.get_cmd_item(target_name, packet["packet_name"], 'RECEIVED_TIMESECONDS', type: :CONVERTED, scope: scope) next unless cur_time @@ -305,9 +305,9 @@ def get_cmd_time(target_name = nil, command_name = nil, scope: $openc3_scope, to command_name = packet["packet_name"] end end - target_name = nil unless command_name - return [target_name, command_name, time.to_i, ((time.to_f - time.to_i) * 1_000_000).to_i] end + target_name = nil unless command_name + return [target_name, command_name, time.to_i, ((time.to_f - time.to_i) * 1_000_000).to_i] end end @@ -330,6 +330,9 @@ def get_cmd_cnt(target_name, command_name, scope: $openc3_scope, token: $openc3_ # @return [Numeric] Transmit count for the command def get_cmd_cnts(target_commands, scope: $openc3_scope, token: $openc3_token) authorize(permission: 'system', scope: scope, token: token) + unless target_commands.is_a?(Array) and target_commands[0].is_a?(Array) + raise "get_cmd_cnts takes an array of arrays containing target, packet_name, e.g. [['INST', 'COLLECT'], ['INST', 'ABORT']]" + end counts = [] target_commands.each do |target_name, command_name| target_name = target_name.upcase diff --git a/openc3/lib/openc3/script/script.rb b/openc3/lib/openc3/script/script.rb index 14f6ac18c4..8d3e47e19d 100644 --- a/openc3/lib/openc3/script/script.rb +++ b/openc3/lib/openc3/script/script.rb @@ -92,14 +92,38 @@ def shutdown_script $script_runner_api_server = nil end - def disconnect_script - $disconnect = true + # This isn't part of the public API because users should use wait() + def openc3_script_sleep(sleep_time = nil) + if sleep_time + sleep(sleep_time) + else + prompt("Press any key to continue...") + end + return false + end + + # Internal method used in scripts when encountering a hazardous command + def prompt_for_hazardous(target_name, cmd_name, hazardous_description) + loop do + message = "Warning: Command #{target_name} #{cmd_name} is Hazardous. " + message << "\n#{hazardous_description}\n" if hazardous_description + message << "Send? (y): " + print message + answer = gets.chomp + if answer.downcase == 'y' + return true + end + end end ########################################################################### # START PUBLIC API ########################################################################### + def disconnect_script + $disconnect = true + end + # DEPRECATED def play_wav_file(wav_filename) # NOOP @@ -110,15 +134,6 @@ def status_bar(message) # NOOP end - def openc3_script_sleep(sleep_time = nil) - if sleep_time - sleep(sleep_time) - else - prompt("Press any key to continue...") - end - return false - end - def ask_string(question, blank_or_default = false, password = false) answer = '' default = '' @@ -180,19 +195,6 @@ def open_files_dialog(title, message = "Open File(s)", filter:) _file_dialog(title, message, filter) end - def prompt_for_hazardous(target_name, cmd_name, hazardous_description) - loop do - message = "Warning: Command #{target_name} #{cmd_name} is Hazardous. " - message << "\n#{hazardous_description}\n" if hazardous_description - message << "Send? (y): " - print message - answer = gets.chomp - if answer.downcase == 'y' - return true - end - end - end - def prompt(string, text_color: nil, background_color: nil, font_size: nil, font_family: nil, details: nil) print "#{string}: " print "Details: #{details}\n" if details diff --git a/openc3/python/openc3/api/cmd_api.py b/openc3/python/openc3/api/cmd_api.py index f7d401939d..edb2d28766 100644 --- a/openc3/python/openc3/api/cmd_api.py +++ b/openc3/python/openc3/api/cmd_api.py @@ -378,21 +378,21 @@ def get_cmd_time(target_name=None, command_name=None, scope=OPENC3_SCOPE): ) if time is None: time = 0 - return [ + return ( target_name, command_name, int(time), int((time - int(time)) * 1_000_000), - ] + ) else: if not target_name: targets = TargetModel.names(scope=scope) else: targets = [target_name.upper()] + time = 0 + command_name = None for target_name in targets: - time = 0 - command_name = None for packet in TargetModel.packets(target_name, type="CMD", scope=scope): cur_time = CommandDecomTopic.get_cmd_item( target_name, @@ -408,14 +408,14 @@ def get_cmd_time(target_name=None, command_name=None, scope=OPENC3_SCOPE): time = cur_time command_name = packet["packet_name"] - if not command_name: - target_name = None - return [ - target_name, - command_name, - int(time), - int((time - int(time)) * 1_000_000), - ] + if not command_name: + target_name = None + return ( + target_name, + command_name, + int(time), + int((time - int(time)) * 1_000_000), + ) # Get the transmit count for a command packet @@ -442,14 +442,19 @@ def get_cmd_cnt(target_name, command_name, scope=OPENC3_SCOPE): # @return [Numeric] Transmit count for the command def get_cmd_cnts(target_commands, scope=OPENC3_SCOPE): authorize(permission="system", scope=scope) - counts = [] - for target_name, command_name in target_commands: - target_name = target_name.upper() - command_name = command_name.upper() - counts.append( - Topic.get_cnt(f"{scope}__COMMAND__{{{target_name}}}__{command_name}") + if type(target_commands) is list and type(target_commands[0] is list): + counts = [] + for target_name, command_name in target_commands: + target_name = target_name.upper() + command_name = command_name.upper() + counts.append( + Topic.get_cnt(f"{scope}__COMMAND__{{{target_name}}}__{command_name}") + ) + return counts + else: + raise RuntimeError( + "get_cmd_cnts takes a dict of dicts containing target, packet_name, e.g. [['INST', 'COLLECT'], ['INST', 'ABORT']]" ) - return counts def _cmd_implementation( diff --git a/openc3/python/openc3/script/__init__.py b/openc3/python/openc3/script/__init__.py index f4c035937a..14eccee54c 100644 --- a/openc3/python/openc3/script/__init__.py +++ b/openc3/python/openc3/script/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # @@ -35,16 +33,23 @@ def shutdown_script(): API_SERVER.shutdown() +########################################################################### +# START PUBLIC API +########################################################################### + + def disconnect_script(): global DISCONNECT DISCONNECT = True +########################################################################### +# END PUBLIC API +########################################################################### + from .api_shared import * -from .cosmos_api import * from .commands import * from .exceptions import * -from .internal_api import * from .limits import * from .telemetry import * from .metadata import * diff --git a/openc3/python/openc3/script/api_shared.py b/openc3/python/openc3/script/api_shared.py index 01e2495129..6c0a2cce62 100644 --- a/openc3/python/openc3/script/api_shared.py +++ b/openc3/python/openc3/script/api_shared.py @@ -401,7 +401,7 @@ def wait_check(*args, type="CONVERTED", scope=OPENC3_SCOPE): polling_rate, ) = _wait_check_process_args(args) start_time = time.time() - success, value = openc3_script_wait_implementation( + success, value = _openc3_script_wait_implementation( target_name, packet_name, item_name, @@ -756,7 +756,7 @@ def _wait_packet( if not initial_count: initial_count = 0 start_time = time.time() - success, value = openc3_script_wait_implementation( + success, value = _openc3_script_wait_implementation( target_name, packet_name, "RECEIVED_COUNT", @@ -812,7 +812,7 @@ def _execute_wait( scope, ): start_time = time.time() - success, value = openc3_script_wait_implementation( + success, value = _openc3_script_wait_implementation( target_name, packet_name, item_name, @@ -999,7 +999,7 @@ def _openc3_script_wait_implementation( # Wait for a converted telemetry item to pass a comparison -def openc3_script_wait_implementation( +def _openc3_script_wait_implementation( target_name, packet_name, item_name, diff --git a/openc3/python/openc3/script/commands.py b/openc3/python/openc3/script/commands.py index c9355bc935..9f3a367b7d 100644 --- a/openc3/python/openc3/script/commands.py +++ b/openc3/python/openc3/script/commands.py @@ -16,12 +16,12 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. +from datetime import datetime import openc3.script from openc3.environment import OPENC3_SCOPE from openc3.top_level import HazardousError from openc3.utilities.logger import Logger from openc3.utilities.extract import * - from openc3.packets.packet import Packet @@ -240,8 +240,6 @@ def get_cmd_time(target_name=None, command_name=None, scope=OPENC3_SCOPE): ) if type(results) == list: if results[2] and results[3]: - pass - # TODO: Python Time.at equivalent? - # results[2] = Time.at(results[2], results[3]).sys - results.pop(3) + results.pop(3) + results[2] = datetime.fromtimestamp(results[2] + results[3] / 1000000) return results diff --git a/openc3/python/openc3/script/cosmos_api.py b/openc3/python/openc3/script/cosmos_api.py deleted file mode 100644 index 96568de891..0000000000 --- a/openc3/python/openc3/script/cosmos_api.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -cmd_tlm_server.py -""" - -# 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 Lesser General Public License -# as published by the Free Software Foundation; version 3 with -# attribution addendums as found in the LICENSE.txt - -# 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. - -import openc3.script - -DEFAULT_SERVER_MESSAGES_QUEUE_SIZE = 1000 - - -def get_interface_names(): - """The get_interface_names method returns a list of the interfaces in the system in an array. - Syntax / Example: - interface_names = get_interface_names() - """ - return openc3.script.API_SERVER.json_rpc_request("get_interface_names") - - -def connect_interface(interface_name, *params): - """The connect_interface method connects to targets associated with a openc3.script.API_SERVER interface. - Syntax: - connect_interface("", ) - """ - return openc3.script.API_SERVER.json_rpc_request( - "connect_interface", interface_name, *params - ) - - -def disconnect_interface(interface_name): - """The disconnect_interface method disconnects from targets associated with a openc3.script.API_SERVER interface. - Syntax: - disconnect_interface("") - """ - return openc3.script.API_SERVER.json_rpc_request( - "disconnect_interface", interface_name - ) - - -def get_router_names(): - """The get_router_names method returns a list of the routers in the - system in an array. - Syntax: - router_names = get_router_names() - """ - return openc3.script.API_SERVER.json_rpc_request("get_router_names") - - -def get_all_router_info(): - """The get_all_router_info method returns information about all routers. - The return value is an array of arrays where each subarray contains the - router name, connection state, number of connected clients, transmit queue - size, receive queue size, bytes transmitted, bytes received, packets - received, and packets sent. - Syntax: - router_info = get_all_router_info() - """ - return openc3.script.API_SERVER.json_rpc_request("get_all_router_info") - - -def connect_router(router_name, *params): - """The connect_router method connects a openc3.script.API_SERVER router. - Syntax: - connect_router("", ) - """ - return openc3.script.API_SERVER.json_rpc_request( - "connect_router", router_name, *params - ) - - -def disconnect_router(router_name): - """The disconnect_router method disconnects a openc3.script.API_SERVER router. - Syntax: - disconnect_router("") - """ - return openc3.script.API_SERVER.json_rpc_request("disconnect_router", router_name) - - -def get_all_target_info(): - """The get_all_target_info method returns information about all targets. - The return value is an array of arrays where each subarray contains the - target name, interface name, command count, and telemetry count for a target. - Syntax: - target_info = get_all_target_info() - """ - return openc3.script.API_SERVER.json_rpc_request("get_all_target_info") - - -def get_all_interface_info(): - """ """ - return openc3.script.API_SERVER.json_rpc_request("get_all_interface_info") - - -def get_cmd_cnt(target_name, command_name): - """ """ - return openc3.script.API_SERVER.json_rpc_request( - "get_cmd_cnt", target_name, command_name - ) - - -def get_tlm_cnt(target_name, packet_name): - """ """ - return openc3.script.API_SERVER.json_rpc_request( - "get_tlm_cnt", target_name, packet_name - ) - - -def subscribe_server_messages(queue_size=DEFAULT_SERVER_MESSAGES_QUEUE_SIZE): - """ """ - return openc3.script.API_SERVER.json_rpc_request( - "subscribe_server_messages", queue_size - ) - - -def unsubscribe_server_messages(id_): - """ """ - return openc3.script.API_SERVER.json_rpc_request("unsubscribe_server_messages", id_) diff --git a/openc3/python/openc3/script/internal_api.py b/openc3/python/openc3/script/internal_api.py deleted file mode 100644 index 4b6597f414..0000000000 --- a/openc3/python/openc3/script/internal_api.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -internal_api.py -""" - -# 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 Lesser General Public License -# as published by the Free Software Foundation; version 3 with -# attribution addendums as found in the LICENSE.txt - -# 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. - -import openc3.script - - -def cosmos_status(): - """Get the cosmos status api. - Syntax / Example: - status = cosmos_status() - """ - resp = openc3.script.API_SERVER.get( - "/openc3-api/internal/status", headers={"Accept": "application/json"} - ) - return resp.json() - - -def cosmos_health(): - """Get the cosmos health api. - Syntax / Example: - health = cosmos_health() - """ - resp = openc3.script.API_SERVER.get( - "/openc3-api/internal/health", headers={"Accept": "application/json"} - ) - return resp.json() - - -def cosmos_metrics(): - """Get the cosmos metrics api. - Syntax / Example: - metrics = cosmos_metrics() - """ - resp = openc3.script.API_SERVER.get( - "/openc3-api/internal/metrics", headers={"Accept": "plain/txt"} - ) - return resp.text diff --git a/openc3/python/test/api/test_cmd_api.py b/openc3/python/test/api/test_cmd_api.py index dd899edbb3..ddc2401693 100644 --- a/openc3/python/test/api/test_cmd_api.py +++ b/openc3/python/test/api/test_cmd_api.py @@ -623,9 +623,9 @@ def test_get_cmd_time_returns_command_times(self): self.assertLess(abs(result[3] - int((now - int(now)) * 1_000_000)), 50000) def test_get_cmd_time_returns_0_if_no_times_are_set(self): - self.assertEqual(get_cmd_time("INST", "ABORT"), ["INST", "ABORT", 0, 0]) - self.assertEqual(get_cmd_time("INST"), [None, None, 0, 0]) - self.assertEqual(get_cmd_time(), [None, None, 0, 0]) + self.assertEqual(get_cmd_time("INST", "ABORT"), ("INST", "ABORT", 0, 0)) + self.assertEqual(get_cmd_time("INST"), (None, None, 0, 0)) + self.assertEqual(get_cmd_time(), (None, None, 0, 0)) def test_get_cmd_cnt_complains_about_non_existant_targets(self): with self.assertRaisesRegex(RuntimeError, "Packet 'BLAH ABORT' does not exist"): From e7bc08705717a82036a0eaf95019c41119e551b4 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Sun, 15 Oct 2023 23:10:04 -0600 Subject: [PATCH 06/17] limits, router, target apis --- openc3/lib/openc3/api/limits_api.rb | 6 +- openc3/lib/openc3/packets/limits.rb | 24 +- openc3/python/openc3/api/authorized_api.py | 2 - openc3/python/openc3/api/cmd_api.py | 2 - openc3/python/openc3/api/limits_api.py | 502 ++++++++++++++++++ openc3/python/openc3/api/router_api.py | 146 +++++ openc3/python/openc3/api/target_api.py | 63 +++ openc3/python/openc3/api/tlm_api.py | 2 - .../microservices/decom_microservice.py | 1 + .../microservices/router_microservice.py | 95 ++++ openc3/python/openc3/models/target_model.py | 30 ++ openc3/python/openc3/packets/commands.py | 7 +- openc3/python/openc3/packets/limits.py | 281 +++++----- openc3/python/openc3/packets/packet.py | 4 +- openc3/python/openc3/system/system.py | 2 +- .../openc3/topics/limits_event_topic.py | 2 +- openc3/python/openc3/topics/router_topic.py | 119 ++++- openc3/python/test/api/test_limits_api.py | 449 ++++++++++++++++ openc3/python/test/api/test_router_api.py | 188 +++++++ openc3/python/test/api/test_target_api.py | 65 +++ 20 files changed, 1821 insertions(+), 169 deletions(-) create mode 100644 openc3/python/openc3/api/limits_api.py create mode 100644 openc3/python/openc3/api/router_api.py create mode 100644 openc3/python/openc3/api/target_api.py create mode 100644 openc3/python/openc3/microservices/router_microservice.py create mode 100644 openc3/python/test/api/test_limits_api.py create mode 100644 openc3/python/test/api/test_router_api.py create mode 100644 openc3/python/test/api/test_target_api.py diff --git a/openc3/lib/openc3/api/limits_api.rb b/openc3/lib/openc3/api/limits_api.rb index 52a9cd9ca9..f11a5f91cb 100644 --- a/openc3/lib/openc3/api/limits_api.rb +++ b/openc3/lib/openc3/api/limits_api.rb @@ -109,7 +109,7 @@ def get_overall_limits_state(ignored_items = nil, scope: $openc3_scope, token: $ # @param args [String|Array] See the description for calling style # @return [Boolean] Whether limits are enable for the itme def limits_enabled?(*args, scope: $openc3_scope, token: $openc3_token) - target_name, packet_name, item_name = tlm_process_args(args, 'limits_enabled?', scope: scope) + target_name, packet_name, item_name = _tlm_process_args(args, 'limits_enabled?', scope: scope) authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token) return TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)['limits']['enabled'] ? true : false end @@ -124,7 +124,7 @@ def limits_enabled?(*args, scope: $openc3_scope, token: $openc3_token) # # @param args [String|Array] See the description for calling style def enable_limits(*args, scope: $openc3_scope, token: $openc3_token) - target_name, packet_name, item_name = tlm_process_args(args, 'enable_limits', scope: scope) + target_name, packet_name, item_name = _tlm_process_args(args, 'enable_limits', scope: scope) authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope, token: token) packet = TargetModel.packet(target_name, packet_name, scope: scope) found_item = nil @@ -157,7 +157,7 @@ def enable_limits(*args, scope: $openc3_scope, token: $openc3_token) # # @param args [String|Array] See the description for calling style def disable_limits(*args, scope: $openc3_scope, token: $openc3_token) - target_name, packet_name, item_name = tlm_process_args(args, 'disable_limits', scope: scope) + target_name, packet_name, item_name = _tlm_process_args(args, 'disable_limits', scope: scope) authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, scope: scope, token: token) packet = TargetModel.packet(target_name, packet_name, scope: scope) found_item = nil diff --git a/openc3/lib/openc3/packets/limits.rb b/openc3/lib/openc3/packets/limits.rb index 831ff0e191..dc90838480 100644 --- a/openc3/lib/openc3/packets/limits.rb +++ b/openc3/lib/openc3/packets/limits.rb @@ -73,21 +73,21 @@ def groups # @param packet_name [String] The packet name. Must be a defined packet name and not 'LATEST'. # @param item_name [String] The item name def enabled?(target_name, packet_name, item_name) - get_packet(target_name, packet_name).get_item(item_name).limits.enabled + _get_packet(target_name, packet_name).get_item(item_name).limits.enabled end # Enables limit checking for the specified item # # @param (see #enabled?) def enable(target_name, packet_name, item_name) - get_packet(target_name, packet_name).enable_limits(item_name) + _get_packet(target_name, packet_name).enable_limits(item_name) end # Disables limit checking for the specified item # # @param (see #enabled?) def disable(target_name, packet_name, item_name) - get_packet(target_name, packet_name).disable_limits(item_name) + _get_packet(target_name, packet_name).disable_limits(item_name) end # Get the limits for a telemetry item @@ -98,7 +98,7 @@ def disable(target_name, packet_name, item_name) # @param limits_set [String or Symbol or nil] Desired Limits set. nil = current limits set # @return [Array>] +def get_out_of_limits(scope=OPENC3_SCOPE): + authorize(permission="tlm", scope=scope) + return LimitsEventTopic.out_of_limits(scope=scope) + + +# Get the overall limits state which is the worse case of all limits items. +# For example if any limits are YELLOW_LOW or YELLOW_HIGH then the overall limits state is YELLOW. +# If a single limit item then turns RED_HIGH the overall limits state is RED. +# +# @param ignored_items [Array>] Array of [TGT, PKT, ITEM] strings +# to ignore when determining overall state. Note, ITEM can be nil to indicate to ignore entire packet. +# @return [String] The overall limits state for the system, one of 'GREEN', 'YELLOW', 'RED' +def get_overall_limits_state(ignored_items=None, scope=OPENC3_SCOPE): + # We only need to check out of limits items so call get_out_of_limits() which authorizes + out_of_limits = get_out_of_limits(scope=scope) + overall = "GREEN" + + # Build easily matchable ignore list + if ignored_items is not None: + new_items = [] + for item in ignored_items: + if len(item) != 3: + raise RuntimeError( + f"Invalid ignored item: {item}. Must be [TGT, PKT, ITEM] where ITEM can be None." + ) + + new_items.append("__").join(item) + ignored_items = new_items + else: + ignored_items = [] + + for target_name, packet_name, item_name, limits_state in out_of_limits: + # Ignore this item if we match one of the ignored items + for item in ignored_items: + if f"{target_name}__{packet_name}__{item_name}" in item: + continue + + if ( + limits_state == "RED" + or limits_state == "RED_HIGH" + or limits_state == "RED_LOW" + ): + overall = limits_state + break # Red is as high as we go so no need to look for more + + # If our overall state is currently blue or green we can go to any state + if overall in ["BLUE", "GREEN", "GREEN_HIGH", "GREEN_LOW"]: + overall = limits_state + # else YELLOW - Stay at YELLOW until we find a red + + if overall == "GREEN_HIGH" or overall == "GREEN_LOW" or overall == "BLUE": + overall = "GREEN" + if overall == "YELLOW_HIGH" or overall == "YELLOW_LOW": + overall = "YELLOW" + if overall == "RED_HIGH" or overall == "RED_LOW": + overall = "RED" + return overall + + +# Whether the limits are enabled for the given item +# +# Accepts two different calling styles: +# limits_enabled("TGT PKT ITEM") +# limits_enabled('TGT','PKT','ITEM') +# +# Favor the first syntax where possible as it is more succinct. +# +# @param args [String|Array] See the description for calling style +# @return [Boolean] Whether limits are enable for the itme +def limits_enabled(*args, scope=OPENC3_SCOPE): + target_name, packet_name, item_name = _tlm_process_args( + args, "limits_enabled?", scope=scope + ) + authorize( + permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope + ) + item = TargetModel.packet_item(target_name, packet_name, item_name, scope=scope) + if item["limits"].get("enabled"): + return True + else: + return False + + +# Enable limits checking for a telemetry item +# +# Accepts two different calling styles: +# enable_limits("TGT PKT ITEM") +# enable_limits('TGT','PKT','ITEM') +# +# Favor the first syntax where possible as it is more succinct. +# +# @param args [String|Array] See the description for calling style +def enable_limits(*args, scope=OPENC3_SCOPE): + target_name, packet_name, item_name = _tlm_process_args( + args, "enable_limits", scope=scope + ) + authorize( + permission="tlm_set", + target_name=target_name, + packet_name=packet_name, + scope=scope, + ) + packet = TargetModel.packet(target_name, packet_name, scope=scope) + found_item = None + for item in packet["items"]: + if item["name"] == item_name: + item["limits"]["enabled"] = True + found_item = item + break + if found_item is None: + raise RuntimeError( + f"Item '{target_name} {packet_name} {item_name}' does not exist" + ) + + TargetModel.set_packet(target_name, packet_name, packet, scope=scope) + + message = f"Enabling Limits For '{target_name} {packet_name} {item_name}'" + Logger.info(message, scope=scope) + + event = { + "type": "LIMITS_ENABLE_STATE", + "target_name": target_name, + "packet_name": packet_name, + "item_name": item_name, + "enabled": True, + "time_nsec": to_nsec_from_epoch(datetime.now(timezone.utc)), + "message": message, + } + LimitsEventTopic.write(event, scope=scope) + + +# Disable limit checking for a telemetry item +# +# Accepts two different calling styles: +# disable_limits("TGT PKT ITEM") +# disable_limits('TGT','PKT','ITEM') +# +# Favor the first syntax where possible as it is more succinct. +# +# @param args [String|Array] See the description for calling style +def disable_limits(*args, scope=OPENC3_SCOPE): + target_name, packet_name, item_name = _tlm_process_args( + args, "disable_limits", scope=scope + ) + authorize( + permission="tlm_set", + target_name=target_name, + packet_name=packet_name, + scope=scope, + ) + packet = TargetModel.packet(target_name, packet_name, scope=scope) + found_item = None + for item in packet["items"]: + if item["name"] == item_name: + item["limits"].pop("enabled", None) + found_item = item + break + if found_item is None: + raise RuntimeError( + f"Item '{target_name} {packet_name} {item_name}' does not exist" + ) + + TargetModel.set_packet(target_name, packet_name, packet, scope=scope) + + message = f"Disabling Limits for '{target_name} {packet_name} {item_name}'" + Logger.info(message, scope=scope) + + event = { + "type": "LIMITS_ENABLE_STATE", + "target_name": target_name, + "packet_name": packet_name, + "item_name": item_name, + "enabled": False, + "time_nsec": to_nsec_from_epoch(datetime.now(timezone.utc)), + "message": message, + } + LimitsEventTopic.write(event, scope=scope) + + +# Get a Hash of all the limits sets defined for an item. Hash keys are the limit +# set name in uppercase (note there is always a DEFAULT) and the value is an array +# of limit values: red low, yellow low, yellow high, red high, . +# Green low and green high are optional. +# +# For example: {'DEFAULT' => [-80, -70, 60, 80, -20, 20], +# 'TVAC' => [-25, -10, 50, 55] } +# +# @return [Hash{String => Array}] +def get_limits(target_name, packet_name, item_name, scope=OPENC3_SCOPE): + authorize( + permission="tlm", target_name=target_name, packet_name=packet_name, scope=scope + ) + limits = {} + item = _get_item(target_name, packet_name, item_name, scope=scope) + for key, vals in item["limits"].items(): + if type(vals) != dict: + continue + + limits[key] = [ + vals["red_low"], + vals["yellow_low"], + vals["yellow_high"], + vals["red_high"], + ] + if vals.get("green_low"): + limits[key] += [vals["green_low"], vals["green_high"]] + return limits + + +# Change the limits settings for a given item. By default, a new limits set called 'CUSTOM' +# is created to avoid overriding existing limits. +def set_limits( + target_name, + packet_name, + item_name, + red_low, + yellow_low, + yellow_high, + red_high, + green_low=None, + green_high=None, + limits_set="CUSTOM", + persistence=None, + enabled=True, + scope=OPENC3_SCOPE, +): + authorize( + permission="tlm_set", + target_name=target_name, + packet_name=packet_name, + scope=scope, + ) + if ( + (red_low > yellow_low) + or (yellow_low >= yellow_high) + or (yellow_high > red_high) + ): + raise RuntimeError( + "Invalid limits specified. Ensure yellow limits are within red limits." + ) + if (green_low and green_high) and ( + (yellow_low > green_low) + or (green_low >= green_high) + or (green_high > yellow_high) + ): + raise RuntimeError( + "Invalid limits specified. Ensure green limits are within yellow limits." + ) + packet = TargetModel.packet(target_name, packet_name, scope=scope) + found_item = None + for item in packet["items"]: + if item["name"] == item_name: + if item["limits"]: + if persistence: + item["limits"]["persistence_setting"] = persistence + if enabled: + item["limits"]["enabled"] = True + else: + item["limits"].pop("enabled", None) + limits = {} + limits["red_low"] = red_low + limits["yellow_low"] = yellow_low + limits["yellow_high"] = yellow_high + limits["red_high"] = red_high + if green_low and green_high: + limits["green_low"] = green_low + if green_low and green_high: + limits["green_high"] = green_high + item["limits"][limits_set] = limits + found_item = item + break + else: + raise RuntimeError("Cannot set_limits on item without any limits") + if found_item is None: + raise RuntimeError( + f"Item '{target_name} {packet_name} {item_name}' does not exist" + ) + message = f"Setting '{target_name} {packet_name} {item_name}' limits to {red_low} {yellow_low} {yellow_high} {red_high}" + if green_low and green_high: + message += f" {green_low} {green_high}" + message += ( + f" in set {limits_set} with persistence {persistence} as enabled {enabled}" + ) + Logger.info(message, scope=scope) + + TargetModel.set_packet(target_name, packet_name, packet, scope=scope) + + event = { + "type": "LIMITS_SETTINGS", + "target_name": target_name, + "packet_name": packet_name, + "item_name": item_name, + "red_low": red_low, + "yellow_low": yellow_low, + "yellow_high": yellow_high, + "red_high": red_high, + "green_low": green_low, + "green_high": green_high, + "limits_set": limits_set, + "persistence": persistence, + "enabled": enabled, + "time_nsec": to_nsec_from_epoch(datetime.now(timezone.utc)), + "message": message, + } + LimitsEventTopic.write(event, scope=scope) + + +# Returns all limits_groups and their members +# @since 5.0.0 Returns hash with values +# @return [Hash{String => Array>] +def get_limits_groups(scope=OPENC3_SCOPE): + authorize(permission="tlm", scope=scope) + return TargetModel.limits_groups(scope=scope) + + +# Enables limits for all the items in the group +# +# @param group_name [String] Name of the group to enable +def enable_limits_group(group_name, scope=OPENC3_SCOPE): + _limits_group(group_name, action="enable", scope=scope) + + +# Disables limits for all the items in the group +# +# @param group_name [String] Name of the group to disable +def disable_limits_group(group_name, scope=OPENC3_SCOPE): + _limits_group(group_name, action="disable", scope=scope) + + +# Returns all defined limits sets +# +# @return [Array] All defined limits sets +def get_limits_sets(scope=OPENC3_SCOPE): + authorize(permission="tlm", scope=scope) + return LimitsEventTopic.sets(scope=scope).keys() + + +# Changes the active limits set that applies to all telemetry +# +# @param limits_set [String] The name of the limits set +def set_limits_set(limits_set, scope=OPENC3_SCOPE): + authorize(permission="tlm_set", scope=scope) + message = f"Setting Limits Set: {limits_set}" + Logger.info(message, scope=scope) + LimitsEventTopic.write( + { + "type": "LIMITS_SET", + "set": str(limits_set), + "time_nsec": to_nsec_from_epoch(datetime.now(timezone.utc)), + "message": message, + }, + scope=scope, + ) + + +# Returns the active limits set that applies to all telemetry +# +# @return [String] The current limits set +def get_limits_set(scope=OPENC3_SCOPE): + authorize(permission="tlm", scope=scope) + return LimitsEventTopic.current_set(scope=scope) + + +# Returns limits events starting at the provided offset. Passing nil for an +# offset will return the last received limits event and associated offset. +# +# @param offset [Integer] Offset to start reading limits events. Nil to return +# the last received limits event (if any). +# @param count [Integer] The total number of events returned. Default is 100. +# @return [Hash, Integer] Event hash followed by the offset. The offset can +# be used in subsequent calls to return events from where the last call left off. +# def get_limits_events(offset = None, count= 100, scope=OPENC3_SCOPE): +# authorize(permission= 'tlm', scope= scope) +# LimitsEventTopic.read(offset, count= count, scope= scope) + + +# Enables or disables a limits group +def _limits_group(group_name, action, scope): + authorize(permission="tlm_set", scope=scope) + group_name.upper() + group = get_limits_groups(scope=scope).get(group_name) + if group is None: + raise RuntimeError( + f"LIMITS_GROUP {group_name} undefined. Ensure your telemetry definition contains the line: LIMITS_GROUP {group_name}" + ) + + Logger.info(f"{action.capitalize()} Limits Group: {group_name}", scope=scope) + last_target_name = None + last_packet_name = None + packet = None + for target_name, packet_name, item_name in group: + if last_target_name != target_name or last_packet_name != packet_name: + if last_target_name and last_packet_name: + TargetModel.set_packet( + last_target_name, last_packet_name, packet, scope=scope + ) + packet = TargetModel.packet(target_name, packet_name, scope=scope) + for item in packet["items"]: + if item["name"] == item_name: + if action == "enable": + enabled = True + item["limits"]["enabled"] = True + message = ( + f"Enabling Limits for '{target_name} {packet_name} {item_name}'" + ) + elif action == "disable": + enabled = False + item["limits"].pop("enabled", None) + message = f"Disabling Limits for '{target_name} {packet_name} {item_name}'" + Logger.info(message, scope=scope) + + event = { + "type": "LIMITS_ENABLE_STATE", + "target_name": target_name, + "packet_name": packet_name, + "item_name": item_name, + "enabled": enabled, + "time_nsec": to_nsec_from_epoch(datetime.now(timezone.utc)), + "message": message, + } + LimitsEventTopic.write(event, scope=scope) + break + last_target_name = target_name + last_packet_name = packet_name + if last_target_name and last_packet_name: + TargetModel.set_packet(last_target_name, last_packet_name, packet, scope=scope) + + +# Gets an item. The code below is mostly duplicated from tlm_process_args in tlm_api.rb. +# +# @param target_name [String] target name +# @param packet_name [String] packet name +# @param item_name [String] item name +# @param scope [String] scope +# @return Hash The requested item based on the packet name +def _get_item( + target_name, packet_name, item_name, cache_timeout=0.1, scope=OPENC3_SCOPE +): + # Determine if this item exists, it will raise appropriate errors if not + if packet_name == "LATEST": + packet_name = CvtModel.determine_latest_packet_for_item( + target_name, item_name, cache_timeout, scope + ) + return TargetModel.packet_item(target_name, packet_name, item_name, scope=scope) diff --git a/openc3/python/openc3/api/router_api.py b/openc3/python/openc3/api/router_api.py new file mode 100644 index 0000000000..e8d9191d90 --- /dev/null +++ b/openc3/python/openc3/api/router_api.py @@ -0,0 +1,146 @@ +# Copyright 2023 OpenC3, Inc. +# 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 addums 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. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +from openc3.api import WHITELIST +from openc3.environment import OPENC3_SCOPE +from openc3.utilities.authorization import authorize +from openc3.models.router_model import RouterModel +from openc3.models.router_status_model import RouterStatusModel +from openc3.topics.router_topic import RouterTopic + +WHITELIST.extend( + [ + "get_router", + "get_router_names", + "connect_router", + "disconnect_router", + "start_raw_logging_router", + "stop_raw_logging_router", + "get_all_router_info", + "router_cmd", + "router_protocol_cmd", + ] +) + + +# Get information about an router +# +# @since 5.0.0 +# @param router_name [String] Router name +# @return [Hash] Hash of all the router information +def get_router(router_name, scope=OPENC3_SCOPE): + authorize(permission="system", router_name=router_name, scope=scope) + router = RouterModel.get(name=router_name, scope=scope) + if not router: + raise RuntimeError(f"Router '{router_name}' does not exist") + return router | RouterStatusModel.get(name=router_name, scope=scope) + + +# @return [Array] All the router names +def get_router_names(scope=OPENC3_SCOPE): + authorize(permission="system", scope=scope) + return RouterModel.names(scope=scope) + + +# Connects an router and starts its telemetry gathering thread +# +# @param router_name [String] The name of the router +# @param router_params [Array] Optional parameters to pass to the router +def connect_router(router_name, *router_params, scope=OPENC3_SCOPE): + authorize(permission="system_set", router_name=router_name, scope=scope) + RouterTopic.connect_router(router_name, *router_params, scope=scope) + + +# Disconnects from an router and kills its telemetry gathering thread +# +# @param router_name [String] The name of the router +def disconnect_router(router_name, scope=OPENC3_SCOPE): + authorize(permission="system_set", router_name=router_name, scope=scope) + RouterTopic.disconnect_router(router_name, scope=scope) + + +# Starts raw logging for an router +# +# @param router_name [String] The name of the router +def start_raw_logging_router(router_name="ALL", scope=OPENC3_SCOPE): + authorize(permission="system_set", router_name=router_name, scope=scope) + if router_name == "ALL": + for router_name in get_router_names(): + RouterTopic.start_raw_logging(router_name, scope=scope) + else: + RouterTopic.start_raw_logging(router_name, scope=scope) + + +# Stop raw logging for an router +# +# @param router_name [String] The name of the router +def stop_raw_logging_router(router_name="ALL", scope=OPENC3_SCOPE): + authorize(permission="system_set", router_name=router_name, scope=scope) + if router_name == "ALL": + for router_name in get_router_names(): + RouterTopic.stop_raw_logging(router_name, scope=scope) + else: + RouterTopic.stop_raw_logging(router_name, scope=scope) + + +# Consolidate all router info into a single API call +# +# @return [Array>] Array of Arrays containing \[name, state, num clients, +# TX queue size, RX queue size, TX bytes, RX bytes, Command count, +# Telemetry count] for all routers +def get_all_router_info(scope=OPENC3_SCOPE): + authorize(permission="system", scope=scope) + info = [] + for _, router in RouterStatusModel.all(scope=scope).items(): + info.append( + [ + router["name"], + router["state"], + router["clients"], + router["txsize"], + router["rxsize"], + router["txbytes"], + router["rxbytes"], + router["rxcnt"], + router["txcnt"], + ] + ) + return info + + +def router_cmd(router_name, cmd_name, *cmd_params, scope=OPENC3_SCOPE): + authorize(permission="system_set", router_name=router_name, scope=scope) + RouterTopic.router_cmd(router_name, cmd_name, *cmd_params, scope=scope) + + +def router_protocol_cmd( + router_name, + cmd_name, + *cmd_params, + read_write="READ_WRITE", + index=-1, + scope=OPENC3_SCOPE, +): + authorize(permission="system_set", router_name=router_name, scope=scope) + RouterTopic.protocol_cmd( + router_name, + cmd_name, + *cmd_params, + read_write=read_write, + index=index, + scope=scope, + ) diff --git a/openc3/python/openc3/api/target_api.py b/openc3/python/openc3/api/target_api.py new file mode 100644 index 0000000000..dbc131f1aa --- /dev/null +++ b/openc3/python/openc3/api/target_api.py @@ -0,0 +1,63 @@ +# Copyright 2022 OpenC3, Inc +# 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. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc.: + +from openc3.api import WHITELIST +from openc3.environment import OPENC3_SCOPE +from openc3.utilities.authorization import authorize +from openc3.models.target_model import TargetModel +from openc3.models.interface_model import InterfaceModel + +WHITELIST.extend( + [ + "get_target_names", + "get_target", + "get_target_interfaces", + ] +) + + +# Returns the list of all target names +# +# @return [Array] All target names +def get_target_names(scope=OPENC3_SCOPE): + authorize(permission="tlm", scope=scope) + return TargetModel.names(scope=scope) + + +# Gets the full target hash +# +# @since 5.0.0 +# @param target_name [String] Target name +# @return [Hash] Hash of all the target properties +def get_target(target_name, scope=OPENC3_SCOPE): + authorize(permission="system", target_name=target_name, scope=scope) + return TargetModel.get(name=target_name, scope=scope) + + +# Get all targets and their interfaces +# +# @return [Array Array>}] + @classmethod + def limits_groups(cls, scope=OPENC3_SCOPE): + groups = Store.hgetall(f"{scope}__limits_groups") + print(f"groups:{groups} type:{type(groups)}") + if groups: + return {k.decode(): json.loads(v) for (k, v) in groups.items()} + else: + return {} + @classmethod def get_item_to_packet_map(cls, target_name, scope=OPENC3_SCOPE): if target_name in TargetModel.item_map_cache: diff --git a/openc3/python/openc3/packets/commands.py b/openc3/python/openc3/packets/commands.py index f9c7f1588e..c8ed441482 100644 --- a/openc3/python/openc3/packets/commands.py +++ b/openc3/python/openc3/packets/commands.py @@ -46,8 +46,9 @@ def warnings(self): # @return [Array] The command target names (excluding UNKNOWN) def target_names(self): - result = self.config.commands.keys().sort() - result.delete("UNKNOWN") + result = list(self.config.commands.keys()) + if "UNKNOWN" in result: + result.remove("UNKNOWN") return result # @param target_name [String] The target name @@ -94,7 +95,7 @@ def identify(self, packet_data, target_names=None): identified_packet = None if not target_names: - target_names = target_names() + target_names = self.target_names() for target_name in target_names: target_name = str(target_name).upper() diff --git a/openc3/python/openc3/packets/limits.py b/openc3/python/openc3/packets/limits.py index 2a8a7cc359..12cf00b302 100644 --- a/openc3/python/openc3/packets/limits.py +++ b/openc3/python/openc3/packets/limits.py @@ -28,8 +28,9 @@ class Limits: # @param config [PacketConfig] Packet configuration to use to access the # limits - def __init__(self, config): + def __init__(self, config, system): self.config = config + self.system = system # (see PacketConfig#warnings) def warnings(self): @@ -51,134 +52,150 @@ def out_of_limits(self): def groups(self): return self.config.limits_groups - -# # Checks whether the limits are enabled for the specified item -# # -# # @param target_name [String] The target name -# # @param packet_name [String] The packet name. Must be a defined packet name and not 'LATEST'. -# # @param item_name [String] The item name -# def enabled?(target_name, packet_name, item_name) -# get_packet(target_name, packet_name).get_item(item_name).limits.enabled -# end - -# # Enables limit checking for the specified item -# # -# # @param (see #enabled?) -# def enable(target_name, packet_name, item_name) -# get_packet(target_name, packet_name).enable_limits(item_name) -# end - -# # Disables limit checking for the specified item -# # -# # @param (see #enabled?) -# def disable(target_name, packet_name, item_name) -# get_packet(target_name, packet_name).disable_limits(item_name) -# end - -# # Get the limits for a telemetry item -# # -# # @param target_name [String] Target Name -# # @param packet_name [String] Packet Name -# # @param item_name [String] Item Name -# # @param limits_set [String or Symbol or nil] Desired Limits set. nil = current limits set -# # @return [Array [] } -# else -# raise "DEFAULT limits must be defined for #{target_name} #{packet_name} #{item_name} before setting limits set #{limits_set}" -# end -# end -# limits_for_set = limits.values[limits_set] -# unless limits_for_set -# limits.values[limits_set] = [] -# limits_for_set = limits.values[limits_set] -# end -# limits_for_set[0] = red_low.to_f -# limits_for_set[1] = yellow_low.to_f -# limits_for_set[2] = yellow_high.to_f -# limits_for_set[3] = red_high.to_f -# limits_for_set.delete_at(5) if limits_for_set[5] -# limits_for_set.delete_at(4) if limits_for_set[4] -# if green_low && green_high -# limits_for_set[4] = green_low.to_f -# limits_for_set[5] = green_high.to_f -# end -# limits.enabled = enabled if not enabled.nil? -# limits.persistence_setting = Integer(persistence) if persistence -# packet.update_limits_items_cache(item) -# @config.limits_sets << limits_set -# @config.limits_sets.uniq! -# return [limits_set, limits.persistence_setting, limits.enabled, limits_for_set[0], limits_for_set[1], limits_for_set[2], limits_for_set[3], limits_for_set[4], limits_for_set[5]] -# end - -# protected - -# def get_packet(target_name, packet_name) -# raise "LATEST packet not valid" if packet_name.upcase == LATEST_PACKET_NAME - -# packets = @config.telemetry[target_name.to_s.upcase] -# raise "Telemetry target '#{target_name.to_s.upcase}' does not exist" unless packets - -# packet = packets[packet_name.to_s.upcase] -# raise "Telemetry packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist" unless packet - -# return packet -# end - -# def includes_item?(ignored_items, target_name, packet_name, item_name) -# ignored_items.each do |array_target_name, array_packet_name, array_item_name| -# if (array_target_name == target_name) && -# (array_packet_name == packet_name) && -# # If the item name is nil we're ignoring an entire packet -# (array_item_name == item_name || array_item_name.nil?) -# return true -# end -# end -# return false -# end -# end -# end + # Checks whether the limits are enabled for the specified item + # + # @param target_name [String] The target name + # @param packet_name [String] The packet name. Must be a defined packet name and not 'LATEST'. + # @param item_name [String] The item name + def enabled(self, target_name, packet_name, item_name): + return ( + self.get_packet(target_name, packet_name).get_item(item_name).limits.enabled + ) + + # Enables limit checking for the specified item + # + # @param (see #enabled?) + def enable(self, target_name, packet_name, item_name): + self.get_packet(target_name, packet_name).enable_limits(item_name) + + # Disables limit checking for the specified item + # + # @param (see #enabled?) + def disable(self, target_name, packet_name, item_name): + self.get_packet(target_name, packet_name).disable_limits(item_name) + + # Get the limits for a telemetry item + # + # @param target_name [String] Target Name + # @param packet_name [String] Packet Name + # @param item_name [String] Item Name + # @param limits_set [String or Symbol or nil] Desired Limits set. nil = current limits set + # @return [Array Date: Mon, 16 Oct 2023 11:18:24 -0600 Subject: [PATCH 07/17] Fix ruff and stuff --- openc3/lib/openc3/api/cmd_api.rb | 7 ++- openc3/python/openc3/api/limits_api.py | 38 ++++++----- .../microservices/decom_microservice.py | 1 - openc3/python/openc3/script/suite_runner.py | 10 +-- openc3/python/openc3/topics/router_topic.py | 3 + openc3/python/test/api/test_limits_api.py | 63 ++++++++++++------- openc3/python/test/api/test_router_api.py | 3 - 7 files changed, 70 insertions(+), 55 deletions(-) diff --git a/openc3/lib/openc3/api/cmd_api.rb b/openc3/lib/openc3/api/cmd_api.rb index 833b203091..653d46d404 100644 --- a/openc3/lib/openc3/api/cmd_api.rb +++ b/openc3/lib/openc3/api/cmd_api.rb @@ -295,14 +295,15 @@ def get_cmd_time(target_name = nil, command_name = nil, scope: $openc3_scope, to end time = 0 command_name = nil - targets.each do |target_name| - TargetModel.packets(target_name, type: :CMD, scope: scope).each do |packet| - cur_time = CommandDecomTopic.get_cmd_item(target_name, packet["packet_name"], 'RECEIVED_TIMESECONDS', type: :CONVERTED, scope: scope) + targets.each do |cur_target| + TargetModel.packets(cur_target, type: :CMD, scope: scope).each do |packet| + cur_time = CommandDecomTopic.get_cmd_item(cur_target, packet["packet_name"], 'RECEIVED_TIMESECONDS', type: :CONVERTED, scope: scope) next unless cur_time if cur_time > time time = cur_time command_name = packet["packet_name"] + target_name = cur_target end end end diff --git a/openc3/python/openc3/api/limits_api.py b/openc3/python/openc3/api/limits_api.py index d470d451bd..0a88b50d0f 100644 --- a/openc3/python/openc3/api/limits_api.py +++ b/openc3/python/openc3/api/limits_api.py @@ -21,12 +21,9 @@ from openc3.api.tlm_api import _tlm_process_args from openc3.environment import OPENC3_SCOPE from openc3.utilities.authorization import authorize -from openc3.topics.topic import Topic from openc3.topics.limits_event_topic import LimitsEventTopic -from openc3.topics.decom_interface_topic import DecomInterfaceTopic from openc3.models.cvt_model import CvtModel from openc3.models.target_model import TargetModel -from openc3.models.interface_model import InterfaceModel # from openc3.utilities.extract import * from openc3.utilities.logger import Logger @@ -81,8 +78,9 @@ def get_overall_limits_state(ignored_items=None, scope=OPENC3_SCOPE): raise RuntimeError( f"Invalid ignored item: {item}. Must be [TGT, PKT, ITEM] where ITEM can be None." ) - - new_items.append("__").join(item) + if item[2] is None: + item[2] = "" + new_items.append("__".join(item)) ignored_items = new_items else: ignored_items = [] @@ -90,21 +88,21 @@ def get_overall_limits_state(ignored_items=None, scope=OPENC3_SCOPE): for target_name, packet_name, item_name, limits_state in out_of_limits: # Ignore this item if we match one of the ignored items for item in ignored_items: - if f"{target_name}__{packet_name}__{item_name}" in item: - continue - - if ( - limits_state == "RED" - or limits_state == "RED_HIGH" - or limits_state == "RED_LOW" - ): - overall = limits_state - break # Red is as high as we go so no need to look for more - - # If our overall state is currently blue or green we can go to any state - if overall in ["BLUE", "GREEN", "GREEN_HIGH", "GREEN_LOW"]: - overall = limits_state - # else YELLOW - Stay at YELLOW until we find a red + if item in f"{target_name}__{packet_name}__{item_name}": + break + else: # Executed if 'for item in ignored_items:' did NOT break + if ( + limits_state == "RED" + or limits_state == "RED_HIGH" + or limits_state == "RED_LOW" + ): + overall = limits_state + break # Red is as high as we go so no need to look for more + + # If our overall state is currently blue or green we can go to any state + if overall in ["BLUE", "GREEN", "GREEN_HIGH", "GREEN_LOW"]: + overall = limits_state + # else YELLOW - Stay at YELLOW until we find a red if overall == "GREEN_HIGH" or overall == "GREEN_LOW" or overall == "BLUE": overall = "GREEN" diff --git a/openc3/python/openc3/microservices/decom_microservice.py b/openc3/python/openc3/microservices/decom_microservice.py index c47588c8f9..fd9bf80b35 100644 --- a/openc3/python/openc3/microservices/decom_microservice.py +++ b/openc3/python/openc3/microservices/decom_microservice.py @@ -164,7 +164,6 @@ def limits_change_callback(self, packet, item, old_limits_state, value, log_chan "time_nsec": time_nsec, "message": str(message), } - print("limits event topic write") LimitsEventTopic.write(event, scope=self.scope) if item.limits.response is not None: diff --git a/openc3/python/openc3/script/suite_runner.py b/openc3/python/openc3/script/suite_runner.py index 31a8770830..efe3a2b774 100644 --- a/openc3/python/openc3/script/suite_runner.py +++ b/openc3/python/openc3/script/suite_runner.py @@ -208,7 +208,7 @@ def build_suites(cls, from_module=None, from_globals=None): "scripts": [], } # Explicitly check for this method and raise an error if it does not exist - if script in group_class: + if script in dir(group_class): cur_suite["groups"][group_class.__name__]["scripts"].append( script ) @@ -225,9 +225,9 @@ def build_suites(cls, from_module=None, from_globals=None): f"{group_class} does not have a {script} method defined." ) - if "setup" in group_class: + if "setup" in dir(group_class): cur_suite["groups"][group_class.__name__]["setup"] = True - if "teardown" in group_class: + if "teardown" in dir(group_class): cur_suite["groups"][group_class.__name__]["teardown"] = True case "GROUP_SETUP": if not cur_suite["groups"].get(group_class.__name__): @@ -237,7 +237,7 @@ def build_suites(cls, from_module=None, from_globals=None): "scripts": [], } # Explicitly check for the setup method and raise an error if it does not exist - if "setup" in group_class: + if "setup" in dir(group_class): cur_suite["groups"][group_class.__name__]["setup"] = True else: raise Exception( @@ -252,7 +252,7 @@ def build_suites(cls, from_module=None, from_globals=None): "scripts": [], } # Explicitly check for the teardown method and raise an error if it does not exist - if "teardown" in group_class: + if "teardown" in dir(group_class): cur_suite["groups"][group_class.__name__]["teardown"] = True else: raise Exception( diff --git a/openc3/python/openc3/topics/router_topic.py b/openc3/python/openc3/topics/router_topic.py index 117baedb9a..111f7cc06f 100644 --- a/openc3/python/openc3/topics/router_topic.py +++ b/openc3/python/openc3/topics/router_topic.py @@ -79,6 +79,9 @@ def route_command(cls, packet, target_names, scope=OPENC3_SCOPE): 100, ) else: + target_name = "UNKNOWN" + if packet.target_name is not None: + target_name = packet.target_name packet_name = "UNKNOWN" if packet.packet_name is not None: packet = packet.packet_name diff --git a/openc3/python/test/api/test_limits_api.py b/openc3/python/test/api/test_limits_api.py index f3c46bc1ff..19c9fcf764 100644 --- a/openc3/python/test/api/test_limits_api.py +++ b/openc3/python/test/api/test_limits_api.py @@ -15,7 +15,7 @@ # if purchased from OpenC3, Inc. import time -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone import unittest import threading from unittest.mock import * @@ -26,15 +26,32 @@ from openc3.topics.telemetry_topic import TelemetryTopic from openc3.models.microservice_model import MicroserviceModel from openc3.microservices.decom_microservice import DecomMicroservice -from openc3.utilities.time import formatted class TestLimitsApi(unittest.TestCase): @patch("openc3.microservices.microservice.System") def setUp(self, system): - mock_redis(self) + redis = mock_redis(self) setup_system() + orig_xread = redis.xread + + # Override xread to ignore the block and count keywords + def xread_side_effect(*args, **kwargs): + result = None + try: + result = orig_xread(*args) + except RuntimeError: + pass + + # # Create a slight delay to simulate the blocking call + if result and len(result) == 0: + time.sleep(0.01) + return result + + redis.xread = Mock() + redis.xread.side_effect = xread_side_effect + # Store Limits Groups for group, items in System.limits.groups().items(): Store.hset("DEFAULT__limits_groups", group, json.dumps(items)) @@ -63,17 +80,17 @@ def tearDown(self): def test_get_limits_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Packet 'BLAH HEALTH_STATUS' does not exist" + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" ): get_limits("BLAH", "HEALTH_STATUS", "TEMP1") def test_get_limits_complains_about_non_existant_packets(self): - with self.assertRaisesRegex(RuntimeError, f"Packet 'INST BLAH' does not exist"): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): get_limits("INST", "BLAH", "TEMP1") def test_get_limits_complains_about_non_existant_items(self): with self.assertRaisesRegex( - RuntimeError, f"Item 'INST HEALTH_STATUS BLAH' does not exist" + RuntimeError, "Item 'INST HEALTH_STATUS BLAH' does not exist" ): get_limits("INST", "HEALTH_STATUS", "BLAH") @@ -108,17 +125,17 @@ def test_gets_limits_for_a_latest_item(self): def test_set_limits_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Packet 'BLAH HEALTH_STATUS' does not exist" + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" ): set_limits("BLAH", "HEALTH_STATUS", "TEMP1", 0.0, 10.0, 20.0, 30.0) def test_set_limits_complains_about_non_existant_packets(self): - with self.assertRaisesRegex(RuntimeError, f"Packet 'INST BLAH' does not exist"): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): set_limits("INST", "BLAH", "TEMP1", 0.0, 10.0, 20.0, 30.0) def test_set_limits_complains_about_non_existant_items(self): with self.assertRaisesRegex( - RuntimeError, f"Item 'INST HEALTH_STATUS BLAH' does not exist" + RuntimeError, "Item 'INST HEALTH_STATUS BLAH' does not exist" ): set_limits("INST", "HEALTH_STATUS", "BLAH", 0.0, 10.0, 20.0, 30.0) @@ -130,9 +147,9 @@ def test_set_limits_creates_a_custom_limits_set(self): ) def test_set_limits_complains_about_invalid_limits(self): - with self.assertRaisesRegex(RuntimeError, f"Invalid limits specified"): + with self.assertRaisesRegex(RuntimeError, "Invalid limits specified"): set_limits("INST", "HEALTH_STATUS", "TEMP1", 2.0, 1.0, 4.0, 5.0) - with self.assertRaisesRegex(RuntimeError, f"Invalid limits specified"): + with self.assertRaisesRegex(RuntimeError, "Invalid limits specified"): set_limits("INST", "HEALTH_STATUS", "TEMP1", 0.0, 1.0, 2.0, 3.0, 4.0, 5.0) def test_set_limits_overrides_existing_limits(self): @@ -215,7 +232,7 @@ def test_get_limits_groups_returns_all_the_limits_groups(self): def test_enable_limits_groups_complains_about_undefined_limits_groups(self): with self.assertRaisesRegex( RuntimeError, - f"LIMITS_GROUP MINE undefined. Ensure your telemetry definition contains the line: LIMITS_GROUP MINE", + "LIMITS_GROUP MINE undefined. Ensure your telemetry definition contains the line: LIMITS_GROUP MINE", ): enable_limits_group("MINE") @@ -231,7 +248,7 @@ def test_enable_limits_groups_enables_limits_for_all_items_in_the_group(self): def test_disable_limits_groups_complains_about_undefined_limits_groups(self): with self.assertRaisesRegex( RuntimeError, - f"LIMITS_GROUP MINE undefined. Ensure your telemetry definition contains the line: LIMITS_GROUP MINE", + "LIMITS_GROUP MINE undefined. Ensure your telemetry definition contains the line: LIMITS_GROUP MINE", ): disable_limits_group("MINE") @@ -381,22 +398,22 @@ def test_get_overall_limits_state_returns_the_overall_system_limits_state(self): def test_get_overall_limits_state_raise_on_invalid_ignored_items(self): with self.assertRaisesRegex(RuntimeError, "Invalid ignored item: BLAH"): get_overall_limits_state(["BLAH"]) - with self.assertRaisesRegex(RuntimeError, f"HEALTH_STATUS"): + with self.assertRaisesRegex(RuntimeError, "HEALTH_STATUS"): get_overall_limits_state([["INST", "HEALTH_STATUS"]]) def test_limits_enabled_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Packet 'BLAH HEALTH_STATUS' does not exist" + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" ): limits_enabled("BLAH", "HEALTH_STATUS", "TEMP1") def test_limits_enabled_complains_about_non_existant_packets(self): - with self.assertRaisesRegex(RuntimeError, f"Packet 'INST BLAH' does not exist"): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): limits_enabled("INST", "BLAH", "TEMP1") def test_limits_enabled_complains_about_non_existant_items(self): with self.assertRaisesRegex( - RuntimeError, f"Item 'INST HEALTH_STATUS BLAH' does not exist" + RuntimeError, "Item 'INST HEALTH_STATUS BLAH' does not exist" ): limits_enabled("INST", "HEALTH_STATUS", "BLAH") @@ -405,17 +422,17 @@ def test_limits_enabled_returns_whether_limits_are_enable_for_an_item(self): def test_enable_limits_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Packet 'BLAH HEALTH_STATUS' does not exist" + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" ): enable_limits("BLAH", "HEALTH_STATUS", "TEMP1") def test_enable_limits_complains_about_non_existant_packets(self): - with self.assertRaisesRegex(RuntimeError, f"Packet 'INST BLAH' does not exist"): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): enable_limits("INST", "BLAH", "TEMP1") def test_enable_limits_complains_about_non_existant_items(self): with self.assertRaisesRegex( - RuntimeError, f"Item 'INST HEALTH_STATUS BLAH' does not exist" + RuntimeError, "Item 'INST HEALTH_STATUS BLAH' does not exist" ): enable_limits("INST", "HEALTH_STATUS", "BLAH") @@ -428,17 +445,17 @@ def test_enable_limits_enables_limits_for_an_item(self): def test_disable_limits_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Packet 'BLAH HEALTH_STATUS' does not exist" + RuntimeError, "Packet 'BLAH HEALTH_STATUS' does not exist" ): disable_limits("BLAH", "HEALTH_STATUS", "TEMP1") def test_disable_limits_complains_about_non_existant_packets(self): - with self.assertRaisesRegex(RuntimeError, f"Packet 'INST BLAH' does not exist"): + with self.assertRaisesRegex(RuntimeError, "Packet 'INST BLAH' does not exist"): disable_limits("INST", "BLAH", "TEMP1") def test_disable_limits_complains_about_non_existant_items(self): with self.assertRaisesRegex( - RuntimeError, f"Item 'INST HEALTH_STATUS BLAH' does not exist" + RuntimeError, "Item 'INST HEALTH_STATUS BLAH' does not exist" ): disable_limits("INST", "HEALTH_STATUS", "BLAH") diff --git a/openc3/python/test/api/test_router_api.py b/openc3/python/test/api/test_router_api.py index c68ec0ffc3..0b02d2df99 100644 --- a/openc3/python/test/api/test_router_api.py +++ b/openc3/python/test/api/test_router_api.py @@ -15,15 +15,12 @@ # if purchased from OpenC3, Inc. import time -import threading import unittest from unittest.mock import * from test.test_helper import * from openc3.api.router_api import * from openc3.interfaces.interface import Interface from openc3.models.router_model import RouterModel -from openc3.models.microservice_model import MicroserviceModel -from openc3.microservices.router_microservice import RouterMicroservice class TestRouterApi(unittest.TestCase): From 64355f6a76a03c8f90da8618581c08a5e6da4a5e Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Mon, 16 Oct 2023 11:27:47 -0600 Subject: [PATCH 08/17] Fix merge --- .../python/examples/cosmos_v5_enterprise_example.py | 3 --- openc3/python/examples/cosmos_v5_example.py | 3 --- openc3/python/openc3/models/cvt_model.py | 12 ++++-------- openc3/python/openc3/packets/packet.py | 7 +------ 4 files changed, 5 insertions(+), 20 deletions(-) diff --git a/openc3/python/examples/cosmos_v5_enterprise_example.py b/openc3/python/examples/cosmos_v5_enterprise_example.py index cd43c90074..e52991692b 100644 --- a/openc3/python/examples/cosmos_v5_enterprise_example.py +++ b/openc3/python/examples/cosmos_v5_enterprise_example.py @@ -12,9 +12,6 @@ from openc3.script import * -print(cosmos_status()) -print(cosmos_health()) - # ~ # telemetry.py print(tlm("INST HEALTH_STATUS TEMP1")) print(tlm_raw("INST HEALTH_STATUS TEMP1")) diff --git a/openc3/python/examples/cosmos_v5_example.py b/openc3/python/examples/cosmos_v5_example.py index e6977e5411..db9c48c0b1 100644 --- a/openc3/python/examples/cosmos_v5_example.py +++ b/openc3/python/examples/cosmos_v5_example.py @@ -11,9 +11,6 @@ from openc3.script import * -print(cosmos_status()) -print(cosmos_health()) - # ~ # telemetry.py print(tlm("INST HEALTH_STATUS TEMP1")) print(tlm_raw("INST HEALTH_STATUS TEMP1")) diff --git a/openc3/python/openc3/models/cvt_model.py b/openc3/python/openc3/models/cvt_model.py index 505cdcc00e..5819df1e9f 100644 --- a/openc3/python/openc3/models/cvt_model.py +++ b/openc3/python/openc3/models/cvt_model.py @@ -270,14 +270,10 @@ def normalize( hash = {} match type: case "ALL": - if item_name in hash: - hash.pop(item_name) - if f"{item_name}__C" in hash: - hash.pop(f"{item_name}__C") - if f"{item_name}__F" in hash: - hash.pop(f"{item_name}__F") - if f"{item_name}__U" in hash: - hash.pop(f"{item_name}__U") + hash.pop(item_name, None) + hash.pop(f"{item_name}__C", None) + hash.pop(f"{item_name}__F", None) + hash.pop(f"{item_name}__U", None) case "RAW": if item_name in hash: hash.pop(item_name) diff --git a/openc3/python/openc3/packets/packet.py b/openc3/python/openc3/packets/packet.py index 6e58fda07d..49803ce69b 100644 --- a/openc3/python/openc3/packets/packet.py +++ b/openc3/python/openc3/packets/packet.py @@ -1055,7 +1055,7 @@ def as_json(self): if self.processors: processors = [] for _, processor in self.processors(): - processors << processor.as_json() + processors.append(processor.as_json()) config["processors"] = processors if self.meta: @@ -1173,12 +1173,7 @@ def handle_limits_values(self, item, value, limits_set, ignore_persistence): limits = None # Retrieve limits settings for the specified limits_set -<<<<<<< HEAD limits = item.limits.values.get(limits_set) -======= - if limits_set in item.limits.values: - limits = item.limits.values[limits_set] ->>>>>>> main # Use the default limits set if limits aren't specified for the: # particular limits set From d706967e2b4effbca700603c8ad39f5cc782ea7f Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Mon, 16 Oct 2023 12:05:25 -0600 Subject: [PATCH 09/17] Fix python unit tests --- openc3/python/openc3/packets/limits.py | 10 ++++++---- openc3/python/test/models/test_cvt_model.py | 3 --- openc3/python/test/streams/test_tcpip_socket_stream.py | 3 +++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openc3/python/openc3/packets/limits.py b/openc3/python/openc3/packets/limits.py index 12cf00b302..0f0ce56319 100644 --- a/openc3/python/openc3/packets/limits.py +++ b/openc3/python/openc3/packets/limits.py @@ -59,20 +59,22 @@ def groups(self): # @param item_name [String] The item name def enabled(self, target_name, packet_name, item_name): return ( - self.get_packet(target_name, packet_name).get_item(item_name).limits.enabled + self._get_packet(target_name, packet_name) + .get_item(item_name) + .limits.enabled ) # Enables limit checking for the specified item # # @param (see #enabled?) def enable(self, target_name, packet_name, item_name): - self.get_packet(target_name, packet_name).enable_limits(item_name) + self._get_packet(target_name, packet_name).enable_limits(item_name) # Disables limit checking for the specified item # # @param (see #enabled?) def disable(self, target_name, packet_name, item_name): - self.get_packet(target_name, packet_name).disable_limits(item_name) + self._get_packet(target_name, packet_name).disable_limits(item_name) # Get the limits for a telemetry item # @@ -82,7 +84,7 @@ def disable(self, target_name, packet_name, item_name): # @param limits_set [String or Symbol or nil] Desired Limits set. nil = current limits set # @return [Array Date: Mon, 16 Oct 2023 12:46:08 -0600 Subject: [PATCH 10/17] Cleanup headers --- .../scripts/running_script.py | 9 ++ openc3/lib/openc3/api/cmd_api.rb | 22 ++-- .../examples/cosmos_v5_stream_example.py | 4 +- openc3/python/openc3/__version__.py | 2 - openc3/python/openc3/api/__init__.py | 2 - openc3/python/openc3/api/limits_api.py | 2 - openc3/python/openc3/api/stash_api.py | 2 +- openc3/python/openc3/api/target_api.py | 2 +- openc3/python/openc3/api/tlm_api.py | 110 +++++++++--------- openc3/python/openc3/config/config_parser.py | 4 +- .../python/openc3/conversions/conversion.py | 2 - .../openc3/conversions/generic_conversion.py | 2 - .../packet_time_formatted_conversion.py | 2 - .../packet_time_seconds_conversion.py | 2 - .../conversions/polynomial_conversion.py | 2 - .../conversions/processor_conversion.py | 2 - .../conversions/received_count_conversion.py | 2 - .../received_time_formatted_conversion.py | 2 - .../received_time_seconds_conversion.py | 2 - .../segmented_polynomial_conversion.py | 2 - .../conversions/unix_time_conversion.py | 2 - .../unix_time_formatted_conversion.py | 2 - .../unix_time_seconds_conversion.py | 2 - openc3/python/openc3/io/json_api_object.py | 2 - openc3/python/openc3/io/json_drb_object.py | 2 - openc3/python/openc3/io/json_rpc.py | 2 - openc3/python/openc3/models/reducer_model.py | 15 +-- openc3/python/openc3/packets/limits.py | 2 - .../python/openc3/packets/limits_response.py | 2 - openc3/python/openc3/packets/packet.py | 2 - openc3/python/openc3/packets/packet_config.py | 2 - openc3/python/openc3/packets/packet_item.py | 2 - .../openc3/packets/packet_item_limits.py | 3 - openc3/python/openc3/packets/structure.py | 2 - .../python/openc3/packets/structure_item.py | 2 - openc3/python/openc3/packets/telemetry.py | 2 - openc3/python/openc3/script/api_shared.py | 4 +- openc3/python/openc3/script/authorization.py | 2 - openc3/python/openc3/script/commands.py | 2 - openc3/python/openc3/script/decorators.py | 9 +- openc3/python/openc3/script/exceptions.py | 2 - openc3/python/openc3/script/limits.py | 2 - openc3/python/openc3/script/metadata.py | 3 - openc3/python/openc3/script/screen.py | 2 - openc3/python/openc3/script/server_proxy.py | 2 - openc3/python/openc3/script/storage.py | 2 - openc3/python/openc3/script/stream.py | 2 +- openc3/python/openc3/script/stream_shared.py | 9 +- openc3/python/openc3/script/suite.py | 2 - openc3/python/openc3/script/suite_results.py | 2 - openc3/python/openc3/script/suite_runner.py | 2 - openc3/python/openc3/script/telemetry.py | 9 +- .../python/openc3/stream_api/base_client.py | 9 +- .../stream_api/data_extractor_client.py | 17 +-- .../openc3/stream_api/log_message_client.py | 9 +- .../python/openc3/utilities/authentication.py | 2 - openc3/python/openc3/utilities/crc.py | 2 +- openc3/python/openc3/utilities/extract.py | 2 +- openc3/python/openc3/utilities/local_mode.py | 2 +- .../python/openc3/utilities/script_shared.py | 2 - .../accessors/test_binary_accessor_read.py | 2 - .../accessors/test_binary_accessor_write.py | 2 - .../test/accessors/test_cbor_accessor.py | 2 - .../test/accessors/test_html_accessor.py | 2 - .../test/accessors/test_json_accessor.py | 2 - .../test/accessors/test_xml_accessor.py | 2 - openc3/python/test/api/test_tlm_api.py | 38 +++--- .../python/test/config/test_config_parser.py | 2 - .../parsers/test_format_string_parser.py | 4 +- .../packets/parsers/test_limits_parser.py | 2 - .../parsers/test_limits_response_parser.py | 2 - .../parsers/test_packet_item_parser.py | 2 - .../packets/parsers/test_packet_parser.py | 2 - .../packets/parsers/test_processor_parser.py | 2 - .../test/packets/parsers/test_state_parser.py | 2 - openc3/python/test/packets/test_packet.py | 2 - .../python/test/packets/test_packet_item.py | 2 - openc3/python/test/packets/test_structure.py | 2 - .../test/packets/test_structure_item.py | 2 - openc3/python/test/script/test_api_shared.py | 2 - openc3/python/test/script/test_telemetry.py | 2 - openc3/python/test/test_authorization.py | 21 +++- openc3/python/test/test_json_rpc_error.py | 21 +++- openc3/python/test/test_json_rpc_request.py | 21 +++- openc3/python/test/test_json_rpc_response.py | 2 - 85 files changed, 170 insertions(+), 301 deletions(-) diff --git a/openc3-cosmos-script-runner-api/scripts/running_script.py b/openc3-cosmos-script-runner-api/scripts/running_script.py index 6922bebafc..60803acbb5 100644 --- a/openc3-cosmos-script-runner-api/scripts/running_script.py +++ b/openc3-cosmos-script-runner-api/scripts/running_script.py @@ -1187,6 +1187,10 @@ def output_thread(self): openc3.script.RUNNING_SCRIPT = RunningScript +########################################################################### +# START PUBLIC API +########################################################################### + def step_mode(): RunningScript.instance.step() @@ -1298,6 +1302,11 @@ def load_utility(procedure_name): return not_cached +########################################################################### +# END PUBLIC API +########################################################################### + + setattr(openc3.script, "load_utility", load_utility) setattr(openc3.script, "require_utility", load_utility) diff --git a/openc3/lib/openc3/api/cmd_api.rb b/openc3/lib/openc3/api/cmd_api.rb index 653d46d404..4455ca105d 100644 --- a/openc3/lib/openc3/api/cmd_api.rb +++ b/openc3/lib/openc3/api/cmd_api.rb @@ -62,39 +62,39 @@ module Api # # Favor the first syntax where possible as it is more succinct. def cmd(*args, **kwargs) - cmd_implementation('cmd', *args, range_check: true, hazardous_check: true, raw: false, **kwargs) + _cmd_implementation('cmd', *args, range_check: true, hazardous_check: true, raw: false, **kwargs) end def cmd_raw(*args, **kwargs) - cmd_implementation('cmd_raw', *args, range_check: true, hazardous_check: true, raw: true, **kwargs) + _cmd_implementation('cmd_raw', *args, range_check: true, hazardous_check: true, raw: true, **kwargs) end # Send a command packet to a target without performing any value range # checks on the parameters. Useful for testing to allow sending command # parameters outside the allowable range as defined in the configuration. def cmd_no_range_check(*args, **kwargs) - cmd_implementation('cmd_no_range_check', *args, range_check: false, hazardous_check: true, raw: false, **kwargs) + _cmd_implementation('cmd_no_range_check', *args, range_check: false, hazardous_check: true, raw: false, **kwargs) end def cmd_raw_no_range_check(*args, **kwargs) - cmd_implementation('cmd_raw_no_range_check', *args, range_check: false, hazardous_check: true, raw: true, **kwargs) + _cmd_implementation('cmd_raw_no_range_check', *args, range_check: false, hazardous_check: true, raw: true, **kwargs) end # Send a command packet to a target without performing any hazardous checks # both on the command itself and its parameters. Useful in scripts to # prevent popping up warnings to the user. def cmd_no_hazardous_check(*args, **kwargs) - cmd_implementation('cmd_no_hazardous_check', *args, range_check: true, hazardous_check: false, raw: false, **kwargs) + _cmd_implementation('cmd_no_hazardous_check', *args, range_check: true, hazardous_check: false, raw: false, **kwargs) end def cmd_raw_no_hazardous_check(*args, **kwargs) - cmd_implementation('cmd_raw_no_hazardous_check', *args, range_check: true, hazardous_check: false, raw: true, **kwargs) + _cmd_implementation('cmd_raw_no_hazardous_check', *args, range_check: true, hazardous_check: false, raw: true, **kwargs) end # Send a command packet to a target without performing any value range # checks or hazardous checks both on the command itself and its parameters. def cmd_no_checks(*args, **kwargs) - cmd_implementation('cmd_no_checks', *args, range_check: false, hazardous_check: false, raw: false, **kwargs) + _cmd_implementation('cmd_no_checks', *args, range_check: false, hazardous_check: false, raw: false, **kwargs) end def cmd_raw_no_checks(*args, **kwargs) - cmd_implementation('cmd_raw_no_checks', *args, range_check: false, hazardous_check: false, raw: true, **kwargs) + _cmd_implementation('cmd_raw_no_checks', *args, range_check: false, hazardous_check: false, raw: true, **kwargs) end # Build a command binary @@ -347,7 +347,7 @@ def get_cmd_cnts(target_commands, scope: $openc3_scope, token: $openc3_token) # PRIVATE implementation details ########################################################################### - def cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw:, timeout: nil, log_message: nil, + def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw:, timeout: nil, log_message: nil, scope: $openc3_scope, token: $openc3_token, **kwargs) extract_string_kwargs_to_args(args, kwargs) unless [nil, true, false].include?(log_message) @@ -403,12 +403,12 @@ def cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw:, end end if log_message - Logger.info(build_cmd_output_string(method_name, target_name, cmd_name, cmd_params, packet), scope: scope) + Logger.info(_build_cmd_output_string(method_name, target_name, cmd_name, cmd_params, packet), scope: scope) end CommandTopic.send_command(command, timeout: timeout, scope: scope) end - def build_cmd_output_string(method_name, target_name, cmd_name, cmd_params, packet) + def _build_cmd_output_string(method_name, target_name, cmd_name, cmd_params, packet) output_string = "#{method_name}(\"" output_string << target_name + ' ' + cmd_name if cmd_params.nil? or cmd_params.empty? diff --git a/openc3/python/examples/cosmos_v5_stream_example.py b/openc3/python/examples/cosmos_v5_stream_example.py index 42cf58e6d7..282ea01afa 100644 --- a/openc3/python/examples/cosmos_v5_stream_example.py +++ b/openc3/python/examples/cosmos_v5_stream_example.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- """ cosmos_v5_stream_example.py """ @@ -19,6 +16,7 @@ from openc3.stream_api.data_extractor_client import DataExtractorClient + def output_data_to_file(data, filename): with open(filename, "w") as f: f.write("ITEM,VALUE,TIME\n") diff --git a/openc3/python/openc3/__version__.py b/openc3/python/openc3/__version__.py index 65fa1d09cc..82a99bcdc3 100644 --- a/openc3/python/openc3/__version__.py +++ b/openc3/python/openc3/__version__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/api/__init__.py b/openc3/python/openc3/api/__init__.py index 55ab3df656..6505cb7f13 100644 --- a/openc3/python/openc3/api/__init__.py +++ b/openc3/python/openc3/api/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/api/limits_api.py b/openc3/python/openc3/api/limits_api.py index 0a88b50d0f..14b1b1ffbf 100644 --- a/openc3/python/openc3/api/limits_api.py +++ b/openc3/python/openc3/api/limits_api.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/api/stash_api.py b/openc3/python/openc3/api/stash_api.py index 723bbeea77..df1713a956 100644 --- a/openc3/python/openc3/api/stash_api.py +++ b/openc3/python/openc3/api/stash_api.py @@ -1,4 +1,4 @@ -# Copyright 2022 OpenC3, Inc +# Copyright 2023 OpenC3, Inc # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it diff --git a/openc3/python/openc3/api/target_api.py b/openc3/python/openc3/api/target_api.py index dbc131f1aa..a8cd8bde1f 100644 --- a/openc3/python/openc3/api/target_api.py +++ b/openc3/python/openc3/api/target_api.py @@ -1,4 +1,4 @@ -# Copyright 2022 OpenC3, Inc +# Copyright 2023 OpenC3, Inc # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it diff --git a/openc3/python/openc3/api/tlm_api.py b/openc3/python/openc3/api/tlm_api.py index 42b756a497..233266563b 100644 --- a/openc3/python/openc3/api/tlm_api.py +++ b/openc3/python/openc3/api/tlm_api.py @@ -14,6 +14,7 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. +import json from openc3.api import WHITELIST from openc3.environment import OPENC3_SCOPE from openc3.utilities.authorization import authorize @@ -43,8 +44,8 @@ "get_all_telemetry_names", "get_telemetry", "get_item", - # 'subscribe_packets', - # 'get_packets', + "subscribe_packets", + "get_packets", "get_tlm_cnt", "get_tlm_cnts", "get_packet_derived_items", @@ -371,56 +372,61 @@ def get_item(target_name, packet_name, item_name, scope=OPENC3_SCOPE): return TargetModel.packet_item(target_name, packet_name, item_name, scope=scope) -# # 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) -# 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| -# target_name = target_name.upper() -# packet_name = packet_name.upper() -# authorize(permission='tlm', target_name= target_name, packet_name= packet_name, scope=scope) -# 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: None, count: 1000, scope=OPENC3_SCOPE) -# authorize(permission='tlm', scope=scope) -# # 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, None, 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 +# 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): + if type(packets) is not list or type(packets[0]) is not list: + raise RuntimeError("packets must be nested array: [['TGT','PKT'],...]") + + result = {} + for target_name, packet_name in packets: + target_name = target_name.upper() + packet_name = packet_name.upper() + authorize( + permission="tlm", + target_name=target_name, + packet_name=packet_name, + scope=scope, + ) + topic = f"{scope}__DECOM__{{{target_name}}}__{packet_name}" + id, _ = Topic.get_newest_message(topic) + if id: + result[topic] = id + else: + result[topic] = "0-0" + return SUBSCRIPTION_DELIMITER.join(result) + + +# 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=None, count=1000, scope=OPENC3_SCOPE): + authorize(permission="tlm", scope=scope) + # Split the list of topic, ID values and turn it into a hash for easy updates + lookup = dict[*id.split(SUBSCRIPTION_DELIMITER)] + xread = Topic.read_topics( + lookup.keys, lookup.values, None, count + ) # Always don't block + # Return the original ID and and empty array if we didn't get anything + packets = [] + if len(xread) == 0: + return [id, packets] + for topic, data in xread: + for id, msg_hash in data: + lookup[topic] = id # save the new ID + json_hash = json.loads(msg_hash["json_data"]) + msg_hash.pop("json_data") + packets.append(msg_hash.merge(json_hash)) + return lookup.to_a.join(SUBSCRIPTION_DELIMITER), packets # Get the receive count for a telemetry packet diff --git a/openc3/python/openc3/config/config_parser.py b/openc3/python/openc3/config/config_parser.py index 0e44d154e8..9c325f69ce 100644 --- a/openc3/python/openc3/config/config_parser.py +++ b/openc3/python/openc3/config/config_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # @@ -30,7 +28,7 @@ class ConfigParser: # Regular expression used to break up an individual line into a keyword and # comma delimited parameters. Handles parameters in single or double quotes. - PARSING_REGEX = "(?:\"(?:[^\\\"]|\\.)*\") | (?:'(?:[^\\']|\\.)*') | \S+" + PARSING_REGEX = r"(?:\"(?:[^\\\"]|\\.)*\") | (?:'(?:[^\\']|\\.)*') | \S+" class Error(Exception): """Error which gets raised by ConfigParser in #verify_num_parameters. This diff --git a/openc3/python/openc3/conversions/conversion.py b/openc3/python/openc3/conversions/conversion.py index 9bc58311f8..28abb01920 100644 --- a/openc3/python/openc3/conversions/conversion.py +++ b/openc3/python/openc3/conversions/conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/generic_conversion.py b/openc3/python/openc3/conversions/generic_conversion.py index 17effdaafa..0b814060eb 100644 --- a/openc3/python/openc3/conversions/generic_conversion.py +++ b/openc3/python/openc3/conversions/generic_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/packet_time_formatted_conversion.py b/openc3/python/openc3/conversions/packet_time_formatted_conversion.py index 4fcf840b92..7853623b66 100644 --- a/openc3/python/openc3/conversions/packet_time_formatted_conversion.py +++ b/openc3/python/openc3/conversions/packet_time_formatted_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/packet_time_seconds_conversion.py b/openc3/python/openc3/conversions/packet_time_seconds_conversion.py index eaafd8b614..35da9fcc9f 100644 --- a/openc3/python/openc3/conversions/packet_time_seconds_conversion.py +++ b/openc3/python/openc3/conversions/packet_time_seconds_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/polynomial_conversion.py b/openc3/python/openc3/conversions/polynomial_conversion.py index b82e0fae47..60f7e106ae 100644 --- a/openc3/python/openc3/conversions/polynomial_conversion.py +++ b/openc3/python/openc3/conversions/polynomial_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/processor_conversion.py b/openc3/python/openc3/conversions/processor_conversion.py index 3c942212e0..f894650d7f 100644 --- a/openc3/python/openc3/conversions/processor_conversion.py +++ b/openc3/python/openc3/conversions/processor_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/received_count_conversion.py b/openc3/python/openc3/conversions/received_count_conversion.py index 48909a61e6..4a27dd3762 100644 --- a/openc3/python/openc3/conversions/received_count_conversion.py +++ b/openc3/python/openc3/conversions/received_count_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/received_time_formatted_conversion.py b/openc3/python/openc3/conversions/received_time_formatted_conversion.py index a29f7f3459..8308913357 100644 --- a/openc3/python/openc3/conversions/received_time_formatted_conversion.py +++ b/openc3/python/openc3/conversions/received_time_formatted_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/received_time_seconds_conversion.py b/openc3/python/openc3/conversions/received_time_seconds_conversion.py index 45364cd73c..9ef010e5a3 100644 --- a/openc3/python/openc3/conversions/received_time_seconds_conversion.py +++ b/openc3/python/openc3/conversions/received_time_seconds_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/segmented_polynomial_conversion.py b/openc3/python/openc3/conversions/segmented_polynomial_conversion.py index d9d8bc952e..105eb372c0 100644 --- a/openc3/python/openc3/conversions/segmented_polynomial_conversion.py +++ b/openc3/python/openc3/conversions/segmented_polynomial_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/unix_time_conversion.py b/openc3/python/openc3/conversions/unix_time_conversion.py index e2d6848dde..95a4688cff 100644 --- a/openc3/python/openc3/conversions/unix_time_conversion.py +++ b/openc3/python/openc3/conversions/unix_time_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/unix_time_formatted_conversion.py b/openc3/python/openc3/conversions/unix_time_formatted_conversion.py index de5592344e..906cd88e28 100644 --- a/openc3/python/openc3/conversions/unix_time_formatted_conversion.py +++ b/openc3/python/openc3/conversions/unix_time_formatted_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/conversions/unix_time_seconds_conversion.py b/openc3/python/openc3/conversions/unix_time_seconds_conversion.py index 6d2a4a28f7..34d6c7fec3 100644 --- a/openc3/python/openc3/conversions/unix_time_seconds_conversion.py +++ b/openc3/python/openc3/conversions/unix_time_seconds_conversion.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/io/json_api_object.py b/openc3/python/openc3/io/json_api_object.py index 2c7f43f85f..7a003eee00 100644 --- a/openc3/python/openc3/io/json_api_object.py +++ b/openc3/python/openc3/io/json_api_object.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/io/json_drb_object.py b/openc3/python/openc3/io/json_drb_object.py index b9f7bf6ac1..63ba285f80 100644 --- a/openc3/python/openc3/io/json_drb_object.py +++ b/openc3/python/openc3/io/json_drb_object.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/io/json_rpc.py b/openc3/python/openc3/io/json_rpc.py index a69cee1084..8e12be3085 100644 --- a/openc3/python/openc3/io/json_rpc.py +++ b/openc3/python/openc3/io/json_rpc.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/models/reducer_model.py b/openc3/python/openc3/models/reducer_model.py index 9bb3cc1210..ee6813ed79 100644 --- a/openc3/python/openc3/models/reducer_model.py +++ b/openc3/python/openc3/models/reducer_model.py @@ -18,6 +18,7 @@ import re from openc3.utilities.store import Store + # Tracks the files which are being stored in buckets for data reduction purposes. # Files are stored in a Redis set by spliting their filenames and storing in # a set named SCOPE__TARGET__reducer__TYPE, e.g. DEFAULT__INST__reducer__decom @@ -25,18 +26,18 @@ # day is the final reduction state. As files are reduced they are removed from # the set. Thus the sets contain the active set of files to be reduced. class ReducerModel: - DECOM_BIN_GZ = re.compile("__decom\.bin.gz$") - REDUCED_MINUTE_BIN_GZ = re.compile("__reduced_minute\.bin.gz$") - REDUCED_HOUR_BIN_GZ = re.compile("__reduced_hour\.bin.gz$") + DECOM_BIN_GZ = re.compile(r"__decom\.bin.gz$") + REDUCED_MINUTE_BIN_GZ = re.compile(r"__reduced_minute\.bin.gz$") + REDUCED_HOUR_BIN_GZ = re.compile(r"__reduced_hour\.bin.gz$") @classmethod def add_file(cls, bucket_key): # Only reduce tlm files - bucket_key_split = bucket_key.split('/') - if bucket_key_split[2] == 'tlm': + bucket_key_split = bucket_key.split("/") + if bucket_key_split[2] == "tlm": # bucket_key is formatted like STARTTIME__ENDTIME__SCOPE__TARGET__PACKET__TYPE.bin # e.g. 20211229191610578229500__20211229192610563836500__DEFAULT__INST__HEALTH_STATUS__rt__decom.bin - _, _, scope, target, _ = os.path.basename(bucket_key).split('__') + _, _, scope, target, _ = os.path.basename(bucket_key).split("__") if cls.DECOM_BIN_GZ.match(bucket_key): return Store.sadd(f"{scope}__{target}__reducer__decom", bucket_key) elif cls.REDUCED_MINUTE_BIN_GZ.match(bucket_key): @@ -47,7 +48,7 @@ def add_file(cls, bucket_key): @classmethod def rm_file(cls, bucket_key): - _, _, scope, target, _ = bucket_key.split('__') + _, _, scope, target, _ = bucket_key.split("__") if cls.DECOM_BIN_GZ.match(bucket_key): return Store.srem(f"{scope}__{target}__reducer__decom", bucket_key) elif cls.REDUCED_MINUTE_BIN_GZ.match(bucket_key): diff --git a/openc3/python/openc3/packets/limits.py b/openc3/python/openc3/packets/limits.py index 0f0ce56319..cf1366b1d2 100644 --- a/openc3/python/openc3/packets/limits.py +++ b/openc3/python/openc3/packets/limits.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/packets/limits_response.py b/openc3/python/openc3/packets/limits_response.py index a9467fe7e8..72ab1c5f1f 100644 --- a/openc3/python/openc3/packets/limits_response.py +++ b/openc3/python/openc3/packets/limits_response.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/packets/packet.py b/openc3/python/openc3/packets/packet.py index 49803ce69b..66cdc4ff3a 100644 --- a/openc3/python/openc3/packets/packet.py +++ b/openc3/python/openc3/packets/packet.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/packets/packet_config.py b/openc3/python/openc3/packets/packet_config.py index cca0d80698..e5f965b53a 100644 --- a/openc3/python/openc3/packets/packet_config.py +++ b/openc3/python/openc3/packets/packet_config.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/packets/packet_item.py b/openc3/python/openc3/packets/packet_item.py index 2e5ccd0bb9..602498c43e 100644 --- a/openc3/python/openc3/packets/packet_item.py +++ b/openc3/python/openc3/packets/packet_item.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/packets/packet_item_limits.py b/openc3/python/openc3/packets/packet_item_limits.py index 352300560d..f96095b8ca 100644 --- a/openc3/python/openc3/packets/packet_item_limits.py +++ b/openc3/python/openc3/packets/packet_item_limits.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # @@ -17,7 +15,6 @@ # if purchased from OpenC3, Inc. - class PacketItemLimits: """Maintains knowledge of limits for a PacketItem""" diff --git a/openc3/python/openc3/packets/structure.py b/openc3/python/openc3/packets/structure.py index 6c167dc1fa..9feddd61f3 100644 --- a/openc3/python/openc3/packets/structure.py +++ b/openc3/python/openc3/packets/structure.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/packets/structure_item.py b/openc3/python/openc3/packets/structure_item.py index e5fe059da2..ff382f9252 100644 --- a/openc3/python/openc3/packets/structure_item.py +++ b/openc3/python/openc3/packets/structure_item.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/packets/telemetry.py b/openc3/python/openc3/packets/telemetry.py index 9914bda8cb..edf83d2c18 100644 --- a/openc3/python/openc3/packets/telemetry.py +++ b/openc3/python/openc3/packets/telemetry.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/api_shared.py b/openc3/python/openc3/script/api_shared.py index 6c0a2cce62..5b824a0717 100644 --- a/openc3/python/openc3/script/api_shared.py +++ b/openc3/python/openc3/script/api_shared.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # @@ -14,7 +12,7 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/script/authorization.py b/openc3/python/openc3/script/authorization.py index b79f3dcf2d..fc04ffdccb 100644 --- a/openc3/python/openc3/script/authorization.py +++ b/openc3/python/openc3/script/authorization.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/commands.py b/openc3/python/openc3/script/commands.py index 9f3a367b7d..5736581afc 100644 --- a/openc3/python/openc3/script/commands.py +++ b/openc3/python/openc3/script/commands.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/decorators.py b/openc3/python/openc3/script/decorators.py index 4971b6a665..7ae68fa17d 100644 --- a/openc3/python/openc3/script/decorators.py +++ b/openc3/python/openc3/script/decorators.py @@ -1,10 +1,3 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -decorators.py -""" - # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # @@ -14,7 +7,7 @@ # attribution addendums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/script/exceptions.py b/openc3/python/openc3/script/exceptions.py index c87f2203e7..f149a8b31c 100644 --- a/openc3/python/openc3/script/exceptions.py +++ b/openc3/python/openc3/script/exceptions.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/limits.py b/openc3/python/openc3/script/limits.py index 7db3f911f0..7745111a59 100644 --- a/openc3/python/openc3/script/limits.py +++ b/openc3/python/openc3/script/limits.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/metadata.py b/openc3/python/openc3/script/metadata.py index ccd8aebf70..7d34634016 100644 --- a/openc3/python/openc3/script/metadata.py +++ b/openc3/python/openc3/script/metadata.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/screen.py b/openc3/python/openc3/script/screen.py index 9d4dd211e2..25f716966a 100644 --- a/openc3/python/openc3/script/screen.py +++ b/openc3/python/openc3/script/screen.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/server_proxy.py b/openc3/python/openc3/script/server_proxy.py index e83815eb05..37359fab4e 100644 --- a/openc3/python/openc3/script/server_proxy.py +++ b/openc3/python/openc3/script/server_proxy.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/storage.py b/openc3/python/openc3/script/storage.py index 4aeb8bf8a8..9a9094772a 100644 --- a/openc3/python/openc3/script/storage.py +++ b/openc3/python/openc3/script/storage.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/stream.py b/openc3/python/openc3/script/stream.py index b3dfe62bf2..bc752abbe3 100644 --- a/openc3/python/openc3/script/stream.py +++ b/openc3/python/openc3/script/stream.py @@ -7,7 +7,7 @@ # attribution addendums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/script/stream_shared.py b/openc3/python/openc3/script/stream_shared.py index 3084c62b98..23915c1240 100644 --- a/openc3/python/openc3/script/stream_shared.py +++ b/openc3/python/openc3/script/stream_shared.py @@ -1,10 +1,3 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -stream_api.py -""" - # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # @@ -14,7 +7,7 @@ # attribution addendums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/script/suite.py b/openc3/python/openc3/script/suite.py index a8f8b2aefa..f3c61a88d5 100644 --- a/openc3/python/openc3/script/suite.py +++ b/openc3/python/openc3/script/suite.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/suite_results.py b/openc3/python/openc3/script/suite_results.py index 831a28a3cc..3c49fdc4d2 100644 --- a/openc3/python/openc3/script/suite_results.py +++ b/openc3/python/openc3/script/suite_results.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/suite_runner.py b/openc3/python/openc3/script/suite_runner.py index efe3a2b774..7747a46931 100644 --- a/openc3/python/openc3/script/suite_runner.py +++ b/openc3/python/openc3/script/suite_runner.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/script/telemetry.py b/openc3/python/openc3/script/telemetry.py index ef97f57b4f..e2b2809e0a 100644 --- a/openc3/python/openc3/script/telemetry.py +++ b/openc3/python/openc3/script/telemetry.py @@ -1,10 +1,3 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -telemetry.py -""" - # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # @@ -14,7 +7,7 @@ # attribution addums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/stream_api/base_client.py b/openc3/python/openc3/stream_api/base_client.py index 281a8adc70..70ba17b575 100644 --- a/openc3/python/openc3/stream_api/base_client.py +++ b/openc3/python/openc3/stream_api/base_client.py @@ -1,10 +1,3 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -base_client.py -""" - # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # @@ -14,7 +7,7 @@ # attribution addendums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/stream_api/data_extractor_client.py b/openc3/python/openc3/stream_api/data_extractor_client.py index 696acc1741..0940df9901 100644 --- a/openc3/python/openc3/stream_api/data_extractor_client.py +++ b/openc3/python/openc3/stream_api/data_extractor_client.py @@ -1,10 +1,3 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -data_extractor_client.py -""" - # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # @@ -14,7 +7,7 @@ # attribution addendums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -67,11 +60,11 @@ def _validate_args( items_ = [] for item in items: - #item_list = item.split(".") - #if len(item_list) != 3: + # item_list = item.split(".") + # if len(item_list) != 3: # raise ValueError(f"incorrect item format: {item}") - #item_list.insert(0, "TLM") - #items_.append("__".join(item_list)) + # item_list.insert(0, "TLM") + # items_.append("__".join(item_list)) items_.append(item) return { diff --git a/openc3/python/openc3/stream_api/log_message_client.py b/openc3/python/openc3/stream_api/log_message_client.py index 2e4234eec7..cbfbbe7241 100644 --- a/openc3/python/openc3/stream_api/log_message_client.py +++ b/openc3/python/openc3/stream_api/log_message_client.py @@ -1,10 +1,3 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -log_message_client.py -""" - # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # @@ -14,7 +7,7 @@ # attribution addendums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/utilities/authentication.py b/openc3/python/openc3/utilities/authentication.py index 99a4c5a514..95c30e1707 100644 --- a/openc3/python/openc3/utilities/authentication.py +++ b/openc3/python/openc3/utilities/authentication.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/openc3/utilities/crc.py b/openc3/python/openc3/utilities/crc.py index 2555e647be..dbe34c81bc 100644 --- a/openc3/python/openc3/utilities/crc.py +++ b/openc3/python/openc3/utilities/crc.py @@ -7,7 +7,7 @@ # attribution addendums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/utilities/extract.py b/openc3/python/openc3/utilities/extract.py index defd7e64b9..705044cd4c 100644 --- a/openc3/python/openc3/utilities/extract.py +++ b/openc3/python/openc3/utilities/extract.py @@ -7,7 +7,7 @@ # attribution addendums as found in the LICENSE.txt # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2023, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license diff --git a/openc3/python/openc3/utilities/local_mode.py b/openc3/python/openc3/utilities/local_mode.py index 9ec799bddc..516e9054c5 100644 --- a/openc3/python/openc3/utilities/local_mode.py +++ b/openc3/python/openc3/utilities/local_mode.py @@ -1,4 +1,4 @@ -# Copyright 2022 OpenC3, Inc. +# Copyright 2023 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it diff --git a/openc3/python/openc3/utilities/script_shared.py b/openc3/python/openc3/utilities/script_shared.py index 91f9642636..bbcb6e0722 100644 --- a/openc3/python/openc3/utilities/script_shared.py +++ b/openc3/python/openc3/utilities/script_shared.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/accessors/test_binary_accessor_read.py b/openc3/python/test/accessors/test_binary_accessor_read.py index d24095e1f9..77e1f4faf2 100644 --- a/openc3/python/test/accessors/test_binary_accessor_read.py +++ b/openc3/python/test/accessors/test_binary_accessor_read.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/accessors/test_binary_accessor_write.py b/openc3/python/test/accessors/test_binary_accessor_write.py index e80fd5a1c4..028418d2e7 100644 --- a/openc3/python/test/accessors/test_binary_accessor_write.py +++ b/openc3/python/test/accessors/test_binary_accessor_write.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/accessors/test_cbor_accessor.py b/openc3/python/test/accessors/test_cbor_accessor.py index 8c624afb78..c339ef59ef 100644 --- a/openc3/python/test/accessors/test_cbor_accessor.py +++ b/openc3/python/test/accessors/test_cbor_accessor.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/accessors/test_html_accessor.py b/openc3/python/test/accessors/test_html_accessor.py index f20d675eff..872f7d64da 100644 --- a/openc3/python/test/accessors/test_html_accessor.py +++ b/openc3/python/test/accessors/test_html_accessor.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/accessors/test_json_accessor.py b/openc3/python/test/accessors/test_json_accessor.py index 4ea55a0c3f..ccae31b714 100644 --- a/openc3/python/test/accessors/test_json_accessor.py +++ b/openc3/python/test/accessors/test_json_accessor.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/accessors/test_xml_accessor.py b/openc3/python/test/accessors/test_xml_accessor.py index 6551bd10f8..16306ed3d8 100644 --- a/openc3/python/test/accessors/test_xml_accessor.py +++ b/openc3/python/test/accessors/test_xml_accessor.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/api/test_tlm_api.py b/openc3/python/test/api/test_tlm_api.py index e6331e5947..098d2144f5 100644 --- a/openc3/python/test/api/test_tlm_api.py +++ b/openc3/python/test/api/test_tlm_api.py @@ -902,7 +902,7 @@ def test_get_tlm_values_marks_data_as_stale(self): # packet = System.telemetry.packet("INST", "HEALTH_STATUS") # packet.received_time = datetime.now(timezone.utc) # packet.write("DURATION", 1.0) - # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") # time.sleep(0.01) # id = subscribe_packets([["inst", "Health_Status"], ["INST", "ADCS"]]) @@ -911,31 +911,31 @@ def test_get_tlm_values_marks_data_as_stale(self): # # Write some packets that should be returned and one that will not # packet.received_time = datetime.now(timezone.utc) # packet.write("DURATION", 2.0) - # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") # packet.received_time = datetime.now(timezone.utc) # packet.write("DURATION", 3.0) - # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") # packet = System.telemetry.packet("INST", "ADCS") # packet.received_time = datetime.now(timezone.utc) - # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") - # packet = System.telemetry.packet("INST", "IMAGE") # Not subscribed + # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + # packet = System.telemetry.packet("INST", "IMAGE") # Not subscribed # packet.received_time = datetime.now(timezone.utc) - # TelemetryDecomTopic.write_packet(packet, scope= "DEFAULT") + # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") # id, packets = get_packets(id) - # packets.each_with_index do |packet, index| - # self.assertEqual(packet['target_name'], "INST") - # match index: - # case 0: - # self.assertEqual(packet['packet_name'], "HEALTH_STATUS") - # self.assertEqual(packet['DURATION'], 2.0) - # case 1: - # self.assertEqual(packet['packet_name'], "HEALTH_STATUS") - # self.assertEqual(packet['DURATION'], 3.0) - # case 2: - # self.assertEqual(packet['packet_name'], "ADCS") - # else: - # raise "Found too many packets" + # for index, packet in enumerate(packets): + # self.assertEqual(packet["target_name"], "INST") + # match index: + # case 0: + # self.assertEqual(packet["packet_name"], "HEALTH_STATUS") + # self.assertEqual(packet["DURATION"], 2.0) + # case 1: + # self.assertEqual(packet["packet_name"], "HEALTH_STATUS") + # self.assertEqual(packet["DURATION"], 3.0) + # case 2: + # self.assertEqual(packet["packet_name"], "ADCS") + # case _: + # raise "Found too many packets" def test_get_tlm_cnt_complains_about_non_existant_targets(self): with self.assertRaisesRegex(RuntimeError, "Packet 'BLAH ABORT' does not exist"): diff --git a/openc3/python/test/config/test_config_parser.py b/openc3/python/test/config/test_config_parser.py index fa9cf0290e..85794c016e 100644 --- a/openc3/python/test/config/test_config_parser.py +++ b/openc3/python/test/config/test_config_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/parsers/test_format_string_parser.py b/openc3/python/test/packets/parsers/test_format_string_parser.py index bbf5547425..0263379bec 100644 --- a/openc3/python/test/packets/parsers/test_format_string_parser.py +++ b/openc3/python/test/packets/parsers/test_format_string_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # @@ -71,7 +69,7 @@ def test_complains_about_invalid_format_strings(self): tf.write(' FORMAT_STRING "%*s"\n') tf.seek(0) with self.assertRaisesRegex( - ConfigParser.Error, "Invalid FORMAT_STRING specified for type INT: %\*s" + ConfigParser.Error, r"Invalid FORMAT_STRING specified for type INT: %\*s" ): self.pc.process_file(tf.name, "TGT1") tf.close() diff --git a/openc3/python/test/packets/parsers/test_limits_parser.py b/openc3/python/test/packets/parsers/test_limits_parser.py index 3cc9e85e5e..80b4f5b102 100644 --- a/openc3/python/test/packets/parsers/test_limits_parser.py +++ b/openc3/python/test/packets/parsers/test_limits_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/parsers/test_limits_response_parser.py b/openc3/python/test/packets/parsers/test_limits_response_parser.py index db9e349084..a16ab25d90 100644 --- a/openc3/python/test/packets/parsers/test_limits_response_parser.py +++ b/openc3/python/test/packets/parsers/test_limits_response_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/parsers/test_packet_item_parser.py b/openc3/python/test/packets/parsers/test_packet_item_parser.py index 3871e9cb4d..eaa314cb91 100644 --- a/openc3/python/test/packets/parsers/test_packet_item_parser.py +++ b/openc3/python/test/packets/parsers/test_packet_item_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/parsers/test_packet_parser.py b/openc3/python/test/packets/parsers/test_packet_parser.py index 5324ade0bb..5f913279f7 100644 --- a/openc3/python/test/packets/parsers/test_packet_parser.py +++ b/openc3/python/test/packets/parsers/test_packet_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/parsers/test_processor_parser.py b/openc3/python/test/packets/parsers/test_processor_parser.py index 7427ae6619..fa3cf302c5 100644 --- a/openc3/python/test/packets/parsers/test_processor_parser.py +++ b/openc3/python/test/packets/parsers/test_processor_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/parsers/test_state_parser.py b/openc3/python/test/packets/parsers/test_state_parser.py index da0a21bc37..4575ccfadc 100644 --- a/openc3/python/test/packets/parsers/test_state_parser.py +++ b/openc3/python/test/packets/parsers/test_state_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/test_packet.py b/openc3/python/test/packets/test_packet.py index d35b4316b7..e1849cade7 100644 --- a/openc3/python/test/packets/test_packet.py +++ b/openc3/python/test/packets/test_packet.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/test_packet_item.py b/openc3/python/test/packets/test_packet_item.py index cf7d6c3e8e..1498b9a709 100644 --- a/openc3/python/test/packets/test_packet_item.py +++ b/openc3/python/test/packets/test_packet_item.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/test_structure.py b/openc3/python/test/packets/test_structure.py index fe16a2f8f5..1128602503 100644 --- a/openc3/python/test/packets/test_structure.py +++ b/openc3/python/test/packets/test_structure.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/packets/test_structure_item.py b/openc3/python/test/packets/test_structure_item.py index 709423fb2c..454e4d91c8 100644 --- a/openc3/python/test/packets/test_structure_item.py +++ b/openc3/python/test/packets/test_structure_item.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/script/test_api_shared.py b/openc3/python/test/script/test_api_shared.py index b3762a1ce2..eb780815eb 100644 --- a/openc3/python/test/script/test_api_shared.py +++ b/openc3/python/test/script/test_api_shared.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/script/test_telemetry.py b/openc3/python/test/script/test_telemetry.py index d8fb0b4c86..5f61fec70f 100644 --- a/openc3/python/test/script/test_telemetry.py +++ b/openc3/python/test/script/test_telemetry.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # diff --git a/openc3/python/test/test_authorization.py b/openc3/python/test/test_authorization.py index 5d37ea03b7..7c55040f31 100644 --- a/openc3/python/test/test_authorization.py +++ b/openc3/python/test/test_authorization.py @@ -1,9 +1,18 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -test_authorization.py -""" +# Copyright 2023 OpenC3, Inc. +# 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. +# +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. import unittest from openc3.script.authorization import CosmosAuthorization diff --git a/openc3/python/test/test_json_rpc_error.py b/openc3/python/test/test_json_rpc_error.py index b77f2b721b..c361689be6 100644 --- a/openc3/python/test/test_json_rpc_error.py +++ b/openc3/python/test/test_json_rpc_error.py @@ -1,9 +1,18 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -test_json_rpc_error.py -""" +# Copyright 2023 OpenC3, Inc +# 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. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc.: import unittest from openc3.io.json_rpc import JsonRpcError diff --git a/openc3/python/test/test_json_rpc_request.py b/openc3/python/test/test_json_rpc_request.py index 783d276b6a..43ca3b2013 100644 --- a/openc3/python/test/test_json_rpc_request.py +++ b/openc3/python/test/test_json_rpc_request.py @@ -1,9 +1,18 @@ -#!/usr/bin/env python3 -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 -# -*- coding: latin-1 -*- -""" -test_json_rpc_request.py -""" +# Copyright 2023 OpenC3, Inc +# 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. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc.: import unittest from openc3.io.json_rpc import JsonRpcRequest, RequestError diff --git a/openc3/python/test/test_json_rpc_response.py b/openc3/python/test/test_json_rpc_response.py index 9b0fca3b87..5f4c214357 100644 --- a/openc3/python/test/test_json_rpc_response.py +++ b/openc3/python/test/test_json_rpc_response.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 OpenC3, Inc. # All Rights Reserved. # From 58b5f0c87c0e07faa6a95a0e6f7ecdd0fe212934 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Mon, 16 Oct 2023 15:11:51 -0600 Subject: [PATCH 11/17] Add subscribe_packet --- openc3/python/openc3/api/tlm_api.py | 39 ++++++++----- openc3/python/test/api/test_tlm_api.py | 78 +++++++++++++------------- 2 files changed, 63 insertions(+), 54 deletions(-) diff --git a/openc3/python/openc3/api/tlm_api.py b/openc3/python/openc3/api/tlm_api.py index 233266563b..46d5474c27 100644 --- a/openc3/python/openc3/api/tlm_api.py +++ b/openc3/python/openc3/api/tlm_api.py @@ -397,11 +397,15 @@ def subscribe_packets(packets, scope=OPENC3_SCOPE): ) topic = f"{scope}__DECOM__{{{target_name}}}__{packet_name}" id, _ = Topic.get_newest_message(topic) + if id: result[topic] = id else: result[topic] = "0-0" - return SUBSCRIPTION_DELIMITER.join(result) + mylist = [] + for k, v in result.items(): + mylist += [k, v] + return SUBSCRIPTION_DELIMITER.join(mylist) # Get packets based on ID returned from subscribe_packet. @@ -412,21 +416,26 @@ def subscribe_packets(packets, scope=OPENC3_SCOPE): def get_packets(id, block=None, count=1000, scope=OPENC3_SCOPE): authorize(permission="tlm", scope=scope) # Split the list of topic, ID values and turn it into a hash for easy updates - lookup = dict[*id.split(SUBSCRIPTION_DELIMITER)] - xread = Topic.read_topics( - lookup.keys, lookup.values, None, count - ) # Always don't block - # Return the original ID and and empty array if we didn't get anything + items = id.split(SUBSCRIPTION_DELIMITER) + # Convert it back into a dict to create a lookup + lookup = dict(zip(items[::2], items[1::2])) packets = [] - if len(xread) == 0: - return [id, packets] - for topic, data in xread: - for id, msg_hash in data: - lookup[topic] = id # save the new ID - json_hash = json.loads(msg_hash["json_data"]) - msg_hash.pop("json_data") - packets.append(msg_hash.merge(json_hash)) - return lookup.to_a.join(SUBSCRIPTION_DELIMITER), packets + for topic, msg_id, msg_hash, redis in Topic.read_topics( + lookup.keys(), list(lookup.values()), None, count + ): + # # Return the original ID and and empty array if we didn't get anything + # for topic, data in xread: + # for id, msg_hash in data: + lookup[topic] = id # save the new ID + # decode the binary string keys and values to strings + msg_hash = {k.decode(): v.decode() for (k, v) in msg_hash.items()} + json_hash = json.loads(msg_hash["json_data"]) + msg_hash.pop("json_data") + packets.append(msg_hash | json_hash) + mylist = [] + for k, v in lookup.items(): + mylist += [k, v] + return (SUBSCRIPTION_DELIMITER.join(mylist), packets) # Get the receive count for a telemetry packet diff --git a/openc3/python/test/api/test_tlm_api.py b/openc3/python/test/api/test_tlm_api.py index 098d2144f5..3dab330737 100644 --- a/openc3/python/test/api/test_tlm_api.py +++ b/openc3/python/test/api/test_tlm_api.py @@ -897,45 +897,45 @@ def test_get_tlm_values_marks_data_as_stale(self): self.assertEqual(vals[3][1], "RED_LOW") self.assertIsNone(vals[4][1]) - # def test_streams_packets_since_the_subscription_was_created(self): - # # Write an initial packet that should not be returned - # packet = System.telemetry.packet("INST", "HEALTH_STATUS") - # packet.received_time = datetime.now(timezone.utc) - # packet.write("DURATION", 1.0) - # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") - # time.sleep(0.01) - - # id = subscribe_packets([["inst", "Health_Status"], ["INST", "ADCS"]]) - # time.sleep(0.01) - - # # Write some packets that should be returned and one that will not - # packet.received_time = datetime.now(timezone.utc) - # packet.write("DURATION", 2.0) - # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") - # packet.received_time = datetime.now(timezone.utc) - # packet.write("DURATION", 3.0) - # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") - # packet = System.telemetry.packet("INST", "ADCS") - # packet.received_time = datetime.now(timezone.utc) - # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") - # packet = System.telemetry.packet("INST", "IMAGE") # Not subscribed - # packet.received_time = datetime.now(timezone.utc) - # TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") - - # id, packets = get_packets(id) - # for index, packet in enumerate(packets): - # self.assertEqual(packet["target_name"], "INST") - # match index: - # case 0: - # self.assertEqual(packet["packet_name"], "HEALTH_STATUS") - # self.assertEqual(packet["DURATION"], 2.0) - # case 1: - # self.assertEqual(packet["packet_name"], "HEALTH_STATUS") - # self.assertEqual(packet["DURATION"], 3.0) - # case 2: - # self.assertEqual(packet["packet_name"], "ADCS") - # case _: - # raise "Found too many packets" + def test_streams_packets_since_the_subscription_was_created(self): + # Write an initial packet that should not be returned + packet = System.telemetry.packet("INST", "HEALTH_STATUS") + packet.received_time = datetime.now(timezone.utc) + packet.write("DURATION", 1.0) + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + time.sleep(0.01) + + id = subscribe_packets([["inst", "Health_Status"], ["INST", "ADCS"]]) + time.sleep(0.01) + + # Write some packets that should be returned and one that will not + packet.received_time = datetime.now(timezone.utc) + packet.write("DURATION", 2.0) + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + packet.received_time = datetime.now(timezone.utc) + packet.write("DURATION", 3.0) + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + packet = System.telemetry.packet("INST", "ADCS") + packet.received_time = datetime.now(timezone.utc) + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + packet = System.telemetry.packet("INST", "IMAGE") # Not subscribed + packet.received_time = datetime.now(timezone.utc) + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + + id, packets = get_packets(id) + for index, packet in enumerate(packets): + self.assertEqual(packet["target_name"], "INST") + match index: + case 0: + self.assertEqual(packet["packet_name"], "HEALTH_STATUS") + self.assertEqual(packet["DURATION"], 2.0) + case 1: + self.assertEqual(packet["packet_name"], "HEALTH_STATUS") + self.assertEqual(packet["DURATION"], 3.0) + case 2: + self.assertEqual(packet["packet_name"], "ADCS") + case _: + raise "Found too many packets" def test_get_tlm_cnt_complains_about_non_existant_targets(self): with self.assertRaisesRegex(RuntimeError, "Packet 'BLAH ABORT' does not exist"): From b086b63360fb34fb0e4c3ec9d23e7b70ddb9da67 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Mon, 16 Oct 2023 17:18:38 -0600 Subject: [PATCH 12/17] test_commands --- openc3/python/openc3/api/cmd_api.py | 8 +- openc3/python/openc3/packets/commands.py | 51 ++- openc3/python/openc3/packets/telemetry.py | 4 +- openc3/python/test/api/test_limits_api.py | 26 +- openc3/python/test/api/test_tlm_api.py | 2 +- openc3/python/test/packets/test_commands.py | 399 ++++++++++++++++++++ 6 files changed, 457 insertions(+), 33 deletions(-) create mode 100644 openc3/python/test/packets/test_commands.py diff --git a/openc3/python/openc3/api/cmd_api.py b/openc3/python/openc3/api/cmd_api.py index 6fad90da83..991a375f16 100644 --- a/openc3/python/openc3/api/cmd_api.py +++ b/openc3/python/openc3/api/cmd_api.py @@ -565,18 +565,18 @@ def _cmd_log_string(method_name, target_name, cmd_name, cmd_params, packet): else: item_type = None - if type(value) == str: + if isinstance(value, str): if item_type == "BLOCK" or item_type == "STRING": if not value.isascii(): value = "0x" + simple_formatted(value) else: - value = str(value) + value = f"'{str(value)}'" else: value = convert_to_value(value) if len(value) > 256: - value = value[:255] + "...'" + value = value[:256] + "...'" value = value.replace('"', "'") - elif type(value) == list: + elif isinstance(value, list): value = f"[{', '.join(str(i) for i in value)}]" params.append(f"{key} {value}") params = ", ".join(params) diff --git a/openc3/python/openc3/packets/commands.py b/openc3/python/openc3/packets/commands.py index c8ed441482..3ee3376c37 100644 --- a/openc3/python/openc3/packets/commands.py +++ b/openc3/python/openc3/packets/commands.py @@ -56,7 +56,7 @@ def target_names(self): # target name keyed by the packet name def packets(self, target_name): target_packets = self.config.commands.get(target_name.upper(), None) - if not target_packets: + if target_packets is None: raise RuntimeError(f"Command target '{target_name.upper()}' does not exist") return target_packets @@ -67,7 +67,7 @@ def packets(self, target_name): def packet(self, target_name, packet_name): target_packets = self.packets(target_name) packet = target_packets.get(packet_name.upper(), None) - if not packet: + if packet is None: raise RuntimeError( f"Command packet '{target_name.upper()} {packet_name.upper()}' does not exist" ) @@ -106,24 +106,24 @@ def identify(self, packet_data, target_names=None): # No commands for this target continue - target = self.system.targets[target_name] + target = self.system.targets.get(target_name) if target and target.cmd_unique_id_mode: # Iterate through the packets and see if any represent the buffer - for _, packet in target_packets: + for _, packet in target_packets.items(): if packet.identify(packet_data): identified_packet = packet break else: # Do a hash lookup to quickly identify the packet if len(target_packets) > 0: - packet = target_packets.first[1] + packet = next(iter(target_packets.values())) key = packet.read_id_values(packet_data) hash = self.config.cmd_id_value_hash[target_name] - identified_packet = hash[key] - if not identified_packet: - identified_packet = hash["CATCHALL"] + identified_packet = hash.get(str(key)) + if identified_packet is None: + identified_packet = hash.get("CATCHALL") - if identified_packet: + if identified_packet is not None: identified_packet.received_count += 1 identified_packet = identified_packet.clone() identified_packet.received_time = None @@ -221,16 +221,18 @@ def build_cmd_output_string(self, target_name, cmd_name, cmd_params, raw=False): except KeyError: item_type = None - if type(value) is str: + if isinstance(value, str): if item_type == "BLOCK" or item_type == "STRING": - if value.isascii(): + if not value.isascii(): value = "0x" + simple_formatted(value) + else: + value = f"'{str(value)}'" else: value = str(convert_to_value(value)) if len(value) > 256: - value = value[0:256] + ":'" + value = value[:256] + "...'" value = value.replace('"', "'") - elif type(value) is list: + elif isinstance(value, list): value = f"[{', '.join(str(i) for i in value)}]" params.append(f"{key} {value}") params = (", ").join(params) @@ -260,6 +262,29 @@ def cmd_pkt_hazardous(self, command): return (False, None) + # Returns whether the given command is hazardous. Commands are hazardous + # if they are marked hazardous overall or if any of their hardardous states + # are set. Thus any given parameter values are first applied to the command + # and then checked for hazardous states. + # + # @param target_name (see #packet) + # @param packet_name (see #packet) + # @param params (see #build_cmd) + def cmd_hazardous(self, target_name, packet_name, params={}): + # Build a command without range checking, perform conversions, and don't + # check required parameters since we're not actually using the command. + return self.cmd_pkt_hazardous( + self.build_cmd(target_name, packet_name, params, False, False, False) + ) + + def clear_counters(self): + for target_name, target_packets in self.config.commands.items(): + for packet_name, packet in target_packets.items(): + packet.received_count = 0 + + def all(self): + return self.config.commands + def _set_parameters(self, command, params, range_checking): given_item_names = [] for item_name, value in params.items(): diff --git a/openc3/python/openc3/packets/telemetry.py b/openc3/python/openc3/packets/telemetry.py index edf83d2c18..4b0cbb136c 100644 --- a/openc3/python/openc3/packets/telemetry.py +++ b/openc3/python/openc3/packets/telemetry.py @@ -280,9 +280,9 @@ def identify(self, packet_data, target_names=None): key = packet.read_id_values(packet_data) hash = self.config.tlm_id_value_hash[target_name] identified_packet = hash.get(repr(key)) - if not identified_packet: + if identified_packet is not None: identified_packet = hash.get("CATCHALL") - if identified_packet: + if identified_packet is not None: return identified_packet return None diff --git a/openc3/python/test/api/test_limits_api.py b/openc3/python/test/api/test_limits_api.py index 19c9fcf764..6ab9c12b0d 100644 --- a/openc3/python/test/api/test_limits_api.py +++ b/openc3/python/test/api/test_limits_api.py @@ -261,16 +261,16 @@ def test_disable_limits_groups_disables_limits_for_all_items_in_the_group(self): self.assertFalse(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) self.assertFalse(limits_enabled("INST", "HEALTH_STATUS", "TEMP3")) - # def test_gets_and_set_the_active_limits_set(self): - # self.assertEqual(get_limits_sets, ["DEFAULT", "TVAC"]) - # set_limits_set("TVAC") - # self.assertEqual(get_limits_set, "TVAC") - # set_limits_set("DEFAULT") - # self.assertEqual(get_limits_set, "DEFAULT") - # set_limits_set("TVAC") - # self.assertEqual(get_limits_set, "TVAC") - # set_limits_set("DEFAULT") - # self.assertEqual(get_limits_set, "DEFAULT") + def test_gets_and_set_the_active_limits_set(self): + self.assertEqual(get_limits_sets, ["DEFAULT", "TVAC"]) + set_limits_set("TVAC") + self.assertEqual(get_limits_set, "TVAC") + set_limits_set("DEFAULT") + self.assertEqual(get_limits_set, "DEFAULT") + set_limits_set("TVAC") + self.assertEqual(get_limits_set, "TVAC") + set_limits_set("DEFAULT") + self.assertEqual(get_limits_set, "DEFAULT") # def test_get_limits_events_returns_empty_array_with_no_events(self): # events = get_limits_events() @@ -370,19 +370,19 @@ def test_get_overall_limits_state_returns_the_overall_system_limits_state(self): "GROUND2STATUS": 1, }, ) - time.sleep(0.01) + time.sleep(0.1) self.assertEqual(get_overall_limits_state(), "GREEN") # TEMP1 limits: -80.0 -70.0 60.0 80.0 -20.0 20.0 # TEMP2 limits: -60.0 -55.0 30.0 35.0 inject_tlm( "INST", "HEALTH_STATUS", {"TEMP1": 70, "TEMP2": 32, "TEMP3": 0, "TEMP4": 0} ) # Both YELLOW - time.sleep(0.01) + time.sleep(0.1) self.assertEqual(get_overall_limits_state(), "YELLOW") inject_tlm( "INST", "HEALTH_STATUS", {"TEMP1": -75, "TEMP2": 40, "TEMP3": 0, "TEMP4": 0} ) - time.sleep(0.01) + time.sleep(0.1) self.assertEqual(get_overall_limits_state(), "RED") self.assertEqual(get_overall_limits_state([]), "RED") diff --git a/openc3/python/test/api/test_tlm_api.py b/openc3/python/test/api/test_tlm_api.py index 3dab330737..d18346dbb6 100644 --- a/openc3/python/test/api/test_tlm_api.py +++ b/openc3/python/test/api/test_tlm_api.py @@ -935,7 +935,7 @@ def test_streams_packets_since_the_subscription_was_created(self): case 2: self.assertEqual(packet["packet_name"], "ADCS") case _: - raise "Found too many packets" + raise RuntimeError("Found too many packets") def test_get_tlm_cnt_complains_about_non_existant_targets(self): with self.assertRaisesRegex(RuntimeError, "Packet 'BLAH ABORT' does not exist"): diff --git a/openc3/python/test/packets/test_commands.py b/openc3/python/test/packets/test_commands.py new file mode 100644 index 0000000000..17b4da35fc --- /dev/null +++ b/openc3/python/test/packets/test_commands.py @@ -0,0 +1,399 @@ +# Copyright 2023 OpenC3, Inc. +# 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 addums 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. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +import math +import tempfile +import unittest +from unittest.mock import * +from test.test_helper import * +from openc3.config.config_parser import ConfigParser +from openc3.system.target import Target +from openc3.packets.packet import Packet +from openc3.packets.commands import Commands +from openc3.packets.packet_config import PacketConfig +from openc3.packets.packet_item import PacketItem +from openc3.processors.processor import Processor +from openc3.conversions.generic_conversion import GenericConversion +from openc3.accessors.binary_accessor import BinaryAccessor +from openc3.conversions.packet_time_seconds_conversion import ( + PacketTimeSecondsConversion, +) +from openc3.conversions.received_time_seconds_conversion import ( + ReceivedTimeSecondsConversion, +) +from datetime import datetime + + +class TestCommands(unittest.TestCase): + def setUp(self): + mock_redis(self) + # setup_system() + System.instance_obj = None + + tf = tempfile.NamedTemporaryFile(mode="w") + tf.write("# This is a comment\n") + tf.write("#\n") + tf.write('COMMAND tgt1 pkt1 LITTLE_ENDIAN "TGT1 PKT1 Description"\n') + tf.write(' APPEND_ID_PARAMETER item1 8 UINT 1 1 1 "Item1"\n') + tf.write(' APPEND_PARAMETER item2 8 UINT 0 254 2 "Item2"\n') + tf.write(' APPEND_PARAMETER item3 8 UINT 0 254 3 "Item3"\n') + tf.write(' APPEND_PARAMETER item4 8 UINT 0 254 4 "Item4"\n') + tf.write('COMMAND tgt1 pkt2 LITTLE_ENDIAN "TGT1 PKT2 Description"\n') + tf.write(' APPEND_ID_PARAMETER item1 8 UINT 2 2 2 "Item1"\n') + tf.write(' APPEND_PARAMETER item2 8 UINT 0 255 2 "Item2"\n') + tf.write(' STATE BAD1 0 HAZARDOUS "Hazardous"\n') + tf.write(" STATE BAD2 1 HAZARDOUS\n") + tf.write(" STATE GOOD 2 DISABLE_MESSAGES\n") + tf.write('COMMAND tgt2 pkt3 LITTLE_ENDIAN "TGT2 PKT3 Description"\n') + tf.write(' HAZARDOUS "Hazardous"\n') + tf.write(' APPEND_ID_PARAMETER item1 8 UINT 3 3 3 "Item1"\n') + tf.write(' APPEND_PARAMETER item2 8 UINT 0 255 2 "Item2"\n') + tf.write(" REQUIRED\n") + tf.write('COMMAND tgt2 pkt4 LITTLE_ENDIAN "TGT2 PKT4 Description"\n') + tf.write(' APPEND_ID_PARAMETER item1 8 UINT 4 4 4 "Item1"\n') + tf.write(' APPEND_PARAMETER item2 2056 STRING "Item2"\n') + tf.write(" OVERFLOW TRUNCATE\n") + tf.write('COMMAND tgt2 pkt5 LITTLE_ENDIAN "TGT2 PKT5 Description"\n') + tf.write(' APPEND_ID_PARAMETER item1 8 UINT 5 5 5 "Item1"\n') + tf.write(' APPEND_PARAMETER item2 8 UINT 0 100 0 "Item2"\n') + tf.write(" POLY_WRITE_CONVERSION 0 2\n") + tf.seek(0) + + pc = PacketConfig() + pc.process_file(tf.name, "SYSTEM") + System.targets["TGT1"] = Target("TGT1", os.getcwd()) + self.cmd = Commands(pc, System) + tf.close() + + def test_target_names_returns_an_empty_array_if_no_targets(self): + self.assertEqual(len(Commands(PacketConfig(), System).warnings()), 0) + self.assertEqual(Commands(PacketConfig(), System).target_names(), []) + + def test_target_names_returns_all_target_names(self): + self.assertEqual(self.cmd.target_names(), ["TGT1", "TGT2"]) + + def test_packets_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command target 'TGTX' does not exist" + ): + self.cmd.packets("tgtX") + + def test_packets_returns_all_packets_target_tgt1(self): + pkts = self.cmd.packets("TGT1") + self.assertEqual(len(pkts), 2) + self.assertIn("PKT1", pkts.keys()) + self.assertIn("PKT2", pkts.keys()) + + def test_packets_returns_all_packets_target_tgt2(self): + pkts = self.cmd.packets("TGT2") + self.assertEqual(len(pkts), 3) + self.assertIn("PKT3", pkts.keys()) + self.assertIn("PKT4", pkts.keys()) + self.assertIn("PKT5", pkts.keys()) + + def test_params_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command target 'TGTX' does not exist" + ): + self.cmd.params("TGTX", "PKT1") + + def test_params_complains_about_non_existant_packets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command packet 'TGT1 PKTX' does not exist" + ): + self.cmd.params("TGT1", "PKTX") + + def test_params_returns_all_items_from_packet_tgt1_pkt1(self): + items = self.cmd.params("TGT1", "PKT1") + self.assertEqual(len(items), 9) + for reserved in Packet.RESERVED_ITEM_NAMES: + self.assertIn(reserved, [item.name for item in items]) + self.assertEqual(items[5].name, "ITEM1") + self.assertEqual(items[6].name, "ITEM2") + self.assertEqual(items[7].name, "ITEM3") + self.assertEqual(items[8].name, "ITEM4") + + def test_packet_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command target 'TGTX' does not exist" + ): + self.cmd.packet("tgtX", "pkt1") + + def test_packet_complains_about_non_existant_packets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command packet 'TGT1 PKTX' does not exist" + ): + self.cmd.packet("TGT1", "PKTX") + + def test_packet_returns_the_specified_packet(self): + pkt = self.cmd.packet("TGT1", "PKT1") + self.assertEqual(pkt.target_name, "TGT1") + self.assertEqual(pkt.packet_name, "PKT1") + + def test_identify_return_None_with_a_None_buffer(self): + self.assertIsNone(self.cmd.identify(None)) + + def test_identify_only_checks_the_targets_given(self): + buffer = b"\x01\x02\x03\x04" + pkt = self.cmd.identify(buffer, ["TGT1"]) + self.assertEqual(pkt.read("item1"), 1) + self.assertEqual(pkt.read("item2"), 2) + self.assertEqual(pkt.read("item3"), 3) + self.assertEqual(pkt.read("item4"), 4) + + def test_identify_works_in_unique_id_mode_or_not(self): + System.targets["TGT1"] = Target("TGT1", os.getcwd()) + target = System.targets["TGT1"] + target.cmd_unique_id_mode = False + buffer = b"\x01\x02\x03\x04" + pkt = self.cmd.identify(buffer, ["TGT1"]) + self.assertEqual(pkt.read("item1"), 1) + self.assertEqual(pkt.read("item2"), 2) + self.assertEqual(pkt.read("item3"), 3) + self.assertEqual(pkt.read("item4"), 4) + target.cmd_unique_id_mode = True + buffer = b"\x01\x02\x01\x02" + pkt = self.cmd.identify(buffer, ["TGT1"]) + self.assertEqual(pkt.read("item1"), 1) + self.assertEqual(pkt.read("item2"), 2) + self.assertEqual(pkt.read("item3"), 1) + self.assertEqual(pkt.read("item4"), 2) + target.cmd_unique_id_mode = False + + def test_identify_returns_None_with_unknown_targets_given(self): + buffer = b"\x01\x02\x03\x04" + self.assertIsNone(self.cmd.identify(buffer, ["TGTX"])) + + def test_identify_logs_an_invalid_sized_buffer(self): + for stdout in capture_io(): + buffer = b"\x01\x02\x03" + pkt = self.cmd.identify(buffer) + self.assertEqual(pkt.read("item1"), 1) + self.assertEqual(pkt.read("item2"), 2) + self.assertEqual(pkt.read("item3"), 3) + self.assertEqual(pkt.read("item4"), 0) + self.assertIn( + "TGT1 PKT1 buffer () received with actual packet length of 3 but defined length of 4", + stdout.getvalue(), + ) + + def test_identify_logs_an_invalid_sized_buffer(self): + for stdout in capture_io(): + buffer = b"\x01\x02\x03\x04\x05" + pkt = self.cmd.identify(buffer) + self.assertEqual(pkt.read("item1"), 1) + self.assertEqual(pkt.read("item2"), 2) + self.assertEqual(pkt.read("item3"), 3) + self.assertEqual(pkt.read("item4"), 4) + self.assertIn( + "TGT1 PKT1 buffer () received with actual packet length of 5 but defined length of 4", + stdout.getvalue(), + ) + + def test_identifies_tgt1_pkt1_but_not_affect_the_latest_data_table(self): + buffer = b"\x01\x02\x03\x04" + pkt = self.cmd.identify(buffer) + self.assertEqual(pkt.read("item1"), 1) + self.assertEqual(pkt.read("item2"), 2) + self.assertEqual(pkt.read("item3"), 3) + self.assertEqual(pkt.read("item4"), 4) + + # Now request the packet from the latest data table + pkt = self.cmd.packet("TGT1", "PKT1") + self.assertEqual(pkt.read("item1"), 0) + self.assertEqual(pkt.read("item2"), 0) + self.assertEqual(pkt.read("item3"), 0) + self.assertEqual(pkt.read("item4"), 0) + + def test_identifies_tgt1_pkt2(self): + buffer = b"\x02\x02" + pkt = self.cmd.identify(buffer) + self.assertEqual(pkt.read("item1"), 2) + self.assertEqual(pkt.read("item2"), "GOOD") + + def test_identifies_tgt2_pkt1(self): + buffer = b"\x03\x02" + pkt = self.cmd.identify(buffer) + self.assertEqual(pkt.read("item1"), 3) + self.assertEqual(pkt.read("item2"), 2) + + def test_build_cmd_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command target 'TGTX' does not exist" + ): + self.cmd.build_cmd("tgtX", "pkt1") + + def test_build_cmd_complains_about_non_existant_packets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command packet 'TGT1 PKTX' does not exist" + ): + self.cmd.build_cmd("tgt1", "pktX") + + def test_build_cmd_complains_about_non_existant_items(self): + with self.assertRaisesRegex( + AttributeError, f"Packet item 'TGT1 PKT1 ITEMX' does not exist" + ): + self.cmd.build_cmd("tgt1", "pkt1", {"itemX": 1}) + + def test_build_cmd_creates_a_populated_command_packet_with_default_values(self): + cmd = self.cmd.build_cmd("TGT1", "PKT1") + self.assertEqual(cmd.read("item1"), 1) + self.assertEqual(cmd.read("item2"), 2) + self.assertEqual(cmd.read("item3"), 3) + self.assertEqual(cmd.read("item4"), 4) + + def test_build_cmd_complains_about_out_of_range_item_values(self): + with self.assertRaisesRegex( + RuntimeError, + f"Command parameter 'TGT1 PKT1 ITEM2' = 1000 not in valid range of 0 to 254", + ): + self.cmd.build_cmd("tgt1", "pkt1", {"item2": 1000}) + + def test_build_cmd_ignores_out_of_range_item_values_if_requested(self): + cmd = self.cmd.build_cmd("tgt1", "pkt1", {"item2": 255}, False) + self.assertEqual(cmd.read("item1"), 1) + self.assertEqual(cmd.read("item2"), 255) + self.assertEqual(cmd.read("item3"), 3) + self.assertEqual(cmd.read("item4"), 4) + + def test_build_cmd_creates_a_command_packet_with_override_item_values(self): + items = {"ITEM2": 10, "ITEM4": 11} + cmd = self.cmd.build_cmd("TGT1", "PKT1", items) + self.assertEqual(cmd.read("item1"), 1) + self.assertEqual(cmd.read("item2"), 10) + self.assertEqual(cmd.read("item3"), 3) + self.assertEqual(cmd.read("item4"), 11) + + def test_build_cmd_creates_a_command_packet_with_override_item_value_states(self): + items = {"ITEM2": "GOOD"} + cmd = self.cmd.build_cmd("TGT1", "PKT2", items) + self.assertEqual(cmd.read("item1"), 2) + self.assertEqual(cmd.read("item2"), "GOOD") + self.assertEqual(cmd.read("ITEM2", "RAW"), 2) + + def test_build_cmd_complains_about_missing_required_parameters(self): + with self.assertRaisesRegex( + RuntimeError, f"Required command parameter 'TGT2 PKT3 ITEM2' not given" + ): + self.cmd.build_cmd("tgt2", "pkt3") + + def test_build_cmd_supports_building_raw_commands(self): + items = {"ITEM2": 10} + cmd = self.cmd.build_cmd("TGT2", "PKT5", items, False, False) + self.assertEqual(cmd.raw, False) + self.assertEqual(cmd.read("ITEM2"), 20) + items = {"ITEM2": 10} + cmd = self.cmd.build_cmd("TGT1", "PKT1", items, False, True) + self.assertEqual(cmd.raw, True) + self.assertEqual(cmd.read("ITEM2"), 10) + + def test_build_cmd_resets_the_buffer_size(self): + packet = self.cmd.packet("TGT1", "PKT1") + packet.buffer = b"\x00" * (packet.defined_length + 1) + self.assertEqual(len(packet.buffer), 5) + items = {"ITEM2": 10} + cmd = self.cmd.build_cmd("TGT1", "PKT1", items) + self.assertEqual(cmd.read("ITEM2"), 10) + self.assertEqual(len(cmd.buffer), 4) + + def test_format_creates_a_string_representation_of_a_command(self): + pkt = self.cmd.packet("TGT1", "PKT1") + self.assertEqual( + self.cmd.format(pkt), + 'cmd("TGT1 PKT1 with ITEM1 0, ITEM2 0, ITEM3 0, ITEM4 0")', + ) + + pkt = self.cmd.packet("TGT2", "PKT4") + pkt.write("ITEM2", "HELLO WORLD") + self.assertEqual( + self.cmd.format(pkt), "cmd(\"TGT2 PKT4 with ITEM1 0, ITEM2 'HELLO WORLD'\")" + ) + + pkt = self.cmd.packet("TGT2", "PKT4") + pkt.write("ITEM2", "HELLO WORLD") + pkt.raw = True + self.assertEqual( + self.cmd.format(pkt), + "cmd_raw(\"TGT2 PKT4 with ITEM1 0, ITEM2 'HELLO WORLD'\")", + ) + + # If the string is too big it should truncate it + string = "" + for i in range(0, 256): + string += "A" + pkt.write("ITEM2", string) + pkt.raw = False + result = self.cmd.format(pkt) + self.assertIn("cmd(\"TGT2 PKT4 with ITEM1 0, ITEM2 'AAAAAAAAAAA", result) + self.assertIn("AAAAAAAAAAA...", result) + + def test_format_ignores_parameters(self): + pkt = self.cmd.packet("TGT1", "PKT1") + self.assertEqual( + self.cmd.format(pkt, ["ITEM3", "ITEM4"]), + 'cmd("TGT1 PKT1 with ITEM1 0, ITEM2 0")', + ) + + def test_cmd_hazardous_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command target 'TGTX' does not exist" + ): + self.cmd.cmd_hazardous("tgtX", "pkt1") + + def test_cmd_hazardous_complains_about_non_existant_packets(self): + with self.assertRaisesRegex( + RuntimeError, f"Command packet 'TGT1 PKTX' does not exist" + ): + self.cmd.cmd_hazardous("tgt1", "pktX") + + def test_cmd_hazardous_complains_about_non_existant_items(self): + with self.assertRaisesRegex( + AttributeError, f"Packet item 'TGT1 PKT1 ITEMX' does not exist" + ): + self.cmd.cmd_hazardous("tgt1", "pkt1", {"itemX": 1}) + + def test_cmd_hazardous_returns_true_if_the_command_overall_is_hazardous(self): + hazardous, description = self.cmd.cmd_hazardous("TGT1", "PKT1") + self.assertFalse(hazardous) + self.assertIsNone(description) + hazardous, description = self.cmd.cmd_hazardous("tgt2", "pkt3") + self.assertTrue(hazardous) + self.assertEqual(description, "Hazardous") + + def test_cmd_hazardous_returns_true_if_a_command_parameter_is_hazardous(self): + hazardous, description = self.cmd.cmd_hazardous("TGT1", "PKT2", {"ITEM2": 0}) + self.assertTrue(hazardous) + self.assertEqual(description, "Hazardous") + hazardous, description = self.cmd.cmd_hazardous("TGT1", "PKT2", {"ITEM2": 1}) + self.assertTrue(hazardous) + self.assertEqual(description, "") + hazardous, description = self.cmd.cmd_hazardous("TGT1", "PKT2", {"ITEM2": 2}) + self.assertFalse(hazardous) + self.assertIsNone(description) + + def test_clears_the_received_counters_in_all_packets(self): + self.cmd.packet("TGT1", "PKT1").received_count = 1 + self.cmd.packet("TGT1", "PKT2").received_count = 2 + self.cmd.packet("TGT2", "PKT3").received_count = 3 + self.cmd.packet("TGT2", "PKT4").received_count = 4 + self.cmd.clear_counters() + self.assertEqual(self.cmd.packet("TGT1", "PKT1").received_count, 0) + self.assertEqual(self.cmd.packet("TGT1", "PKT2").received_count, 0) + self.assertEqual(self.cmd.packet("TGT2", "PKT3").received_count, 0) + self.assertEqual(self.cmd.packet("TGT2", "PKT4").received_count, 0) + + def test_returns_all_packets(self): + self.assertEqual(list(self.cmd.all().keys()), ["UNKNOWN", "TGT1", "TGT2"]) From eb29ac60fc25bcc376cd2888cf86da8cf70f874d Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Mon, 16 Oct 2023 18:39:13 -0600 Subject: [PATCH 13/17] Python unit test improvements --- openc3/python/openc3/api/limits_api.py | 10 +- openc3/python/openc3/packets/telemetry.py | 2 +- .../openc3/topics/decom_interface_topic.py | 2 +- .../openc3/topics/limits_event_topic.py | 28 ++- openc3/python/test/api/test_limits_api.py | 175 ++++++++++-------- .../protocols/test_fixed_protocol.py | 6 +- .../protocols/test_ignore_packet_protocol.py | 6 +- openc3/python/test/packets/test_commands.py | 43 ++--- openc3/python/test/test_helper.py | 8 + 9 files changed, 155 insertions(+), 125 deletions(-) diff --git a/openc3/python/openc3/api/limits_api.py b/openc3/python/openc3/api/limits_api.py index 14b1b1ffbf..d7713711ea 100644 --- a/openc3/python/openc3/api/limits_api.py +++ b/openc3/python/openc3/api/limits_api.py @@ -386,7 +386,9 @@ def disable_limits_group(group_name, scope=OPENC3_SCOPE): # @return [Array] All defined limits sets def get_limits_sets(scope=OPENC3_SCOPE): authorize(permission="tlm", scope=scope) - return LimitsEventTopic.sets(scope=scope).keys() + sets = list(LimitsEventTopic.sets(scope=scope).keys()) + sets.sort() + return sets # Changes the active limits set that applies to all telemetry @@ -423,9 +425,9 @@ def get_limits_set(scope=OPENC3_SCOPE): # @param count [Integer] The total number of events returned. Default is 100. # @return [Hash, Integer] Event hash followed by the offset. The offset can # be used in subsequent calls to return events from where the last call left off. -# def get_limits_events(offset = None, count= 100, scope=OPENC3_SCOPE): -# authorize(permission= 'tlm', scope= scope) -# LimitsEventTopic.read(offset, count= count, scope= scope) +def get_limits_events(offset=None, count=100, scope=OPENC3_SCOPE): + authorize(permission="tlm", scope=scope) + return LimitsEventTopic.read(offset, count=count, scope=scope) # Enables or disables a limits group diff --git a/openc3/python/openc3/packets/telemetry.py b/openc3/python/openc3/packets/telemetry.py index 4b0cbb136c..8cf4a09846 100644 --- a/openc3/python/openc3/packets/telemetry.py +++ b/openc3/python/openc3/packets/telemetry.py @@ -280,7 +280,7 @@ def identify(self, packet_data, target_names=None): key = packet.read_id_values(packet_data) hash = self.config.tlm_id_value_hash[target_name] identified_packet = hash.get(repr(key)) - if identified_packet is not None: + if identified_packet is None: identified_packet = hash.get("CATCHALL") if identified_packet is not None: return identified_packet diff --git a/openc3/python/openc3/topics/decom_interface_topic.py b/openc3/python/openc3/topics/decom_interface_topic.py index 49be510299..1b0402b57e 100644 --- a/openc3/python/openc3/topics/decom_interface_topic.py +++ b/openc3/python/openc3/topics/decom_interface_topic.py @@ -45,7 +45,7 @@ def build_cmd( ack_topic = f"{{{scope}__ACKCMD}}TARGET__{target_name}" start_time = time.time() while (time.time() - start_time) < timeout: - for _, _, msg_hash, _ in Topic.read_topics([ack_topic]): + for topic, msg_id, msg_hash, redis in Topic.read_topics([ack_topic]): if msg_hash[b"id"] == decom_id: if msg_hash[b"result"] == b"SUCCESS": msg_hash = { diff --git a/openc3/python/openc3/topics/limits_event_topic.py b/openc3/python/openc3/topics/limits_event_topic.py index e933e2b668..0fa58e2403 100644 --- a/openc3/python/openc3/topics/limits_event_topic.py +++ b/openc3/python/openc3/topics/limits_event_topic.py @@ -20,6 +20,7 @@ from openc3.system.system import System from openc3.utilities.store import Store from openc3.config.config_parser import ConfigParser +from openc3.utilities.json import JsonEncoder, JsonDecoder # LimitsEventTopic keeps track of not only the __openc3_limits_events topic @@ -85,18 +86,21 @@ def write(cls, event, scope): case "LIMITS_SET": sets = cls.sets(scope=scope) - if not sets.has_key(event["set"]): + if sets.get(event["set"]) is None: raise RuntimeError(f"Set '{event['set']}' does not exist!") # Set all existing sets to "false" sets = dict.fromkeys(sets, "false") sets[event["set"]] = "true" # Enable the requested set - Store.hmset(f"{scope}__limits_sets", *sets) + Store.hset(f"{scope}__limits_sets", mapping=sets) case _: raise RuntimeError(f"Invalid limits event type '{event['type']}'") Topic.write_topic( - f"{scope}__openc3_limits_events", {"event": json.dumps(event)}, "*", 1000 + f"{scope}__openc3_limits_events", + {"event": json.dumps(event, cls=JsonEncoder)}, + "*", + 1000, ) # Remove the JSON encoding to return hashes directly @@ -105,18 +109,21 @@ def read(cls, offset=None, count=100, scope=None): final_result = [] topic = f"{scope}__openc3_limits_events" if offset is not None: - result = Topic.read_topics([topic], [offset], None, count) - if len(result) != 0: + for topic, msg_id, msg_hash, redis in Topic.read_topics( + [topic], [offset], None, count + ): + # result = Topic.read_topics([topic], [offset], None, count) + # if len(result) != 0: # result is a hash with the topic key followed by an array of results # This returns just the array of arrays [[offset, hash], [offset, hash], ...] - final_result = result[topic] + final_result.append([msg_id, msg_hash]) else: result = Topic.get_newest_message(topic) if result: final_result = [result] parsed_result = [] for offset, hash in final_result: - parsed_result.append([offset, json.loads(hash[b"event"])]) + parsed_result.append([offset, json.loads(hash[b"event"], cls=JsonDecoder)]) return parsed_result @classmethod @@ -152,7 +159,10 @@ def sets(cls, scope): @classmethod def current_set(cls, scope): - return LimitsEventTopic.sets(scope=scope).key("true") or "DEFAULT" + sets = LimitsEventTopic.sets(scope=scope) + # Lookup the key with a true value because there should only ever be one + current = list(sets.keys())[list(sets.values()).index("true")] + return current or "DEFAULT" # Cleanups up the current_limits and current_limits_settings keys for # a target or target/packet combination @@ -225,7 +235,7 @@ def sync_system_thread_body(cls, scope, block_ms=None): telemetry = System.telemetry.all() topics = [f"{scope}__openc3_limits_events"] for _, _, event, _ in Topic.read_topics(topics, None, block_ms): - event = json.loads(event[b"event"]) + event = json.loads(event[b"event"], cls=JsonDecoder) match event["type"]: case "LIMITS_CHANGE": pass # Ignore diff --git a/openc3/python/test/api/test_limits_api.py b/openc3/python/test/api/test_limits_api.py index 6ab9c12b0d..8d57945d0f 100644 --- a/openc3/python/test/api/test_limits_api.py +++ b/openc3/python/test/api/test_limits_api.py @@ -26,6 +26,7 @@ from openc3.topics.telemetry_topic import TelemetryTopic from openc3.models.microservice_model import MicroserviceModel from openc3.microservices.decom_microservice import DecomMicroservice +from openc3.utilities.time import formatted class TestLimitsApi(unittest.TestCase): @@ -36,11 +37,13 @@ def setUp(self, system): orig_xread = redis.xread - # Override xread to ignore the block and count keywords + # Override xread to ignore the block keyword def xread_side_effect(*args, **kwargs): + if "block" in kwargs: + kwargs.pop("block") result = None try: - result = orig_xread(*args) + result = orig_xread(*args, **kwargs) except RuntimeError: pass @@ -262,82 +265,106 @@ def test_disable_limits_groups_disables_limits_for_all_items_in_the_group(self): self.assertFalse(limits_enabled("INST", "HEALTH_STATUS", "TEMP3")) def test_gets_and_set_the_active_limits_set(self): - self.assertEqual(get_limits_sets, ["DEFAULT", "TVAC"]) + self.assertEqual(get_limits_sets(), ["DEFAULT", "TVAC"]) set_limits_set("TVAC") - self.assertEqual(get_limits_set, "TVAC") + self.assertEqual(get_limits_set(), "TVAC") set_limits_set("DEFAULT") - self.assertEqual(get_limits_set, "DEFAULT") + self.assertEqual(get_limits_set(), "DEFAULT") set_limits_set("TVAC") - self.assertEqual(get_limits_set, "TVAC") + self.assertEqual(get_limits_set(), "TVAC") set_limits_set("DEFAULT") - self.assertEqual(get_limits_set, "DEFAULT") - - # def test_get_limits_events_returns_empty_array_with_no_events(self): - # events = get_limits_events() - # self.assertEqual(events, ([])) - - # def test_get_limits_events_returns_an_offset_and_limits_event_hash(self): - # # Load the events topic with two events ... only the last should be returned - # event = { type= 'LIMITS_CHANGE', target_name= "BLAH", packet_name= "BLAH", item_name= "BLAH", - # old_limits_state= 'RED_LOW', new_limits_state= 'RED_HIGH', time_nsec= 0, message= "nope" } - # LimitsEventTopic.write(event, scope= "DEFAULT") - # time = Time.now.to_nsec_from_epoch - # event = { type= 'LIMITS_CHANGE', target_name= "TGT", packet_name= "PKT", item_name= "ITEM", - # old_limits_state= 'GREEN', new_limits_state= 'YELLOW_LOW', time_nsec= time, message= "message" } - # LimitsEventTopic.write(event, scope= "DEFAULT") - # events = get_limits_events() - # self.assertEqual(events).to be_a Array - # offset = events[0][0] - # event = events[0][1] - # self.assertIn('\d{13}-\d', offset) - # self.assertEqual(event).to be_a Hash - # self.assertEqual(event['type'], "LIMITS_CHANGE") - # self.assertEqual(event['target_name'], "TGT") - # self.assertEqual(event['packet_name'], "PKT") - # self.assertEqual(event['old_limits_state'], "GREEN") - # self.assertEqual(event['new_limits_state'], "YELLOW_LOW") - # self.assertEqual(event['time_nsec'], time) - # self.assertEqual(event['message'], "message") - - # def test_get_limits_events_returns_multiple_offsets/events_with_multiple_calls(self): - # event = { type= 'LIMITS_CHANGE', target_name= "TGT", packet_name= "PKT", item_name= "ITEM", - # old_limits_state= 'GREEN', new_limits_state= 'YELLOW_LOW', time_nsec= 0, message= "message" } - # LimitsEventTopic.write(event, scope= "DEFAULT") - # events = get_limits_events() - # self.assertIn('\d{13}-\d', events[0][0]) - # self.assertEqual(events[0][1]['time_nsec'], 0) - # last_offset = events[-1][0] - - # # Load additional events - # event[:old_limits_state] = 'YELLOW_LOW' - # event[:new_limits_state] = 'RED_LOW' - # event[:time_nsec] = 1 - # LimitsEventTopic.write(event, scope= "DEFAULT") - # event[:old_limits_state] = 'RED_LOW' - # event[:new_limits_state] = 'YELLOW_LOW' - # event[:time_nsec] = 2 - # LimitsEventTopic.write(event, scope= "DEFAULT") - # event[:old_limits_state] = 'YELLOW_LOW' - # event[:new_limits_state] = 'GREEN' - # event[:time_nsec] = 3 - # LimitsEventTopic.write(event, scope= "DEFAULT") - # # Limit the count to 2 - # events = get_limits_events(last_offset, count= 2) - # self.assertEqual(len(events), 2) - # self.assertIn('\d{13}-\d', events[0][0]) - # self.assertEqual(events[0][1]['time_nsec'], 1) - # self.assertIn('\d{13}-\d', events[1][0]) - # self.assertEqual(events[1][1]['time_nsec'], 2) - # last_offset = events[-1][0] - - # events = get_limits_events(last_offset) - # self.assertEqual(len(events), 1) - # self.assertIn('\d{13}-\d', events[0][0]) - # self.assertEqual(events[0][1]['time_nsec'], 3) - # last_offset = events[-1][0] - - # events = get_limits_events(last_offset) - # self.assertEqual(events, ([])) + self.assertEqual(get_limits_set(), "DEFAULT") + + def test_get_limits_events_returns_an_offset_and_limits_event_hash(self): + # Load the events topic with two events ... only the last should be returned + event = { + "type": "LIMITS_CHANGE", + "target_name": "BLAH", + "packet_name": "BLAH", + "item_name": "BLAH", + "old_limits_state": "RED_LOW", + "new_limits_state": "RED_HIGH", + "time_nsec": 0, + "message": "nope", + } + LimitsEventTopic.write(event, scope="DEFAULT") + time = datetime.now(timezone.utc) + event = { + "type": "LIMITS_CHANGE", + "target_name": "TGT", + "packet_name": "PKT", + "item_name": "ITEM", + "old_limits_state": "GREEN", + "new_limits_state": "YELLOW_LOW", + "time_nsec": time, + "message": "message", + } + LimitsEventTopic.write(event, scope="DEFAULT") + events = get_limits_events() + self.assertIsInstance(events, list) + offset = events[0][0] + event = events[0][1] + self.assertRegex(offset, r"\d{13}-\d") + self.assertIsInstance(event, dict) + self.assertEqual(event["type"], "LIMITS_CHANGE") + self.assertEqual(event["target_name"], "TGT") + self.assertEqual(event["packet_name"], "PKT") + self.assertEqual(event["old_limits_state"], "GREEN") + self.assertEqual(event["new_limits_state"], "YELLOW_LOW") + # TODO: This is a different timestamp coming back: + # 2023-10-16 23:48:36.761255 vs our formatted 2023/10/16 23:48:36.761 + self.assertEqual(event["time_nsec"][0:-3], formatted(time).replace("/", "-")) + self.assertEqual(event["message"], "message") + + def test_get_limits_events_returns_multiple_offsets_events_with_multiple_calls( + self, + ): + event = { + "type": "LIMITS_CHANGE", + "target_name": "TGT", + "packet_name": "PKT", + "item_name": "ITEM", + "old_limits_state": "GREEN", + "new_limits_state": "YELLOW_LOW", + "time_nsec": 0, + "message": "message", + } + LimitsEventTopic.write(event, scope="DEFAULT") + events = get_limits_events() + self.assertRegex(events[0][0], r"\d{13}-\d") + self.assertEqual(events[0][1]["time_nsec"], 0) + last_offset = events[-1][0] + + # Load additional events + event["old_limits_state"] = "YELLOW_LOW" + event["new_limits_state"] = "RED_LOW" + event["time_nsec"] = 1 + LimitsEventTopic.write(event, scope="DEFAULT") + event["old_limits_state"] = "RED_LOW" + event["new_limits_state"] = "YELLOW_LOW" + event["time_nsec"] = 2 + LimitsEventTopic.write(event, scope="DEFAULT") + event["old_limits_state"] = "YELLOW_LOW" + event["new_limits_state"] = "GREEN" + event["time_nsec"] = 3 + LimitsEventTopic.write(event, scope="DEFAULT") + # Limit the count to 2 + events = get_limits_events(last_offset, count=2) + self.assertEqual(len(events), 2) + self.assertRegex(events[0][0], r"\d{13}-\d") + self.assertEqual(events[0][1]["time_nsec"], 1) + self.assertRegex(events[1][0], r"\d{13}-\d") + self.assertEqual(events[1][1]["time_nsec"], 2) + last_offset = events[-1][0] + + events = get_limits_events(last_offset) + self.assertEqual(len(events), 1) + self.assertRegex(events[0][0], r"\d{13}-\d") + self.assertEqual(events[0][1]["time_nsec"], 3) + last_offset = events[-1][0] + + events = get_limits_events(last_offset) + self.assertEqual(events, ([])) def test_get_out_of_limits_returns_all_out_of_limits_items(self): inject_tlm( diff --git a/openc3/python/test/interfaces/protocols/test_fixed_protocol.py b/openc3/python/test/interfaces/protocols/test_fixed_protocol.py index 86b46ae184..9b2a79a395 100644 --- a/openc3/python/test/interfaces/protocols/test_fixed_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_fixed_protocol.py @@ -49,11 +49,9 @@ class MyInterface(StreamInterface): def connected(self): return True - @classmethod - def setUpClass(cls): - setup_system() - def setUp(self): + mock_redis(self) + setup_system() self.interface = TestFixedProtocol.MyInterface() def test_initializes_attributes(self): diff --git a/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py b/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py index 512101697b..6dcbde8a8f 100644 --- a/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py @@ -58,11 +58,9 @@ class MyInterface(StreamInterface): def connected(self): return True - @classmethod - def setUpClass(cls): - setup_system() - def setUp(self): + mock_redis(self) + setup_system() self.interface = TestIgnorePacketProtocol.MyInterface() self.interface.target_names = ["SYSTEM", "INST"] self.interface.cmd_target_names = ["SYSTEM", "INST"] diff --git a/openc3/python/test/packets/test_commands.py b/openc3/python/test/packets/test_commands.py index 17b4da35fc..be5001eb83 100644 --- a/openc3/python/test/packets/test_commands.py +++ b/openc3/python/test/packets/test_commands.py @@ -14,27 +14,14 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. -import math import tempfile import unittest from unittest.mock import * from test.test_helper import * -from openc3.config.config_parser import ConfigParser from openc3.system.target import Target from openc3.packets.packet import Packet from openc3.packets.commands import Commands from openc3.packets.packet_config import PacketConfig -from openc3.packets.packet_item import PacketItem -from openc3.processors.processor import Processor -from openc3.conversions.generic_conversion import GenericConversion -from openc3.accessors.binary_accessor import BinaryAccessor -from openc3.conversions.packet_time_seconds_conversion import ( - PacketTimeSecondsConversion, -) -from openc3.conversions.received_time_seconds_conversion import ( - ReceivedTimeSecondsConversion, -) -from datetime import datetime class TestCommands(unittest.TestCase): @@ -87,7 +74,7 @@ def test_target_names_returns_all_target_names(self): def test_packets_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Command target 'TGTX' does not exist" + RuntimeError, "Command target 'TGTX' does not exist" ): self.cmd.packets("tgtX") @@ -106,13 +93,13 @@ def test_packets_returns_all_packets_target_tgt2(self): def test_params_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Command target 'TGTX' does not exist" + RuntimeError, "Command target 'TGTX' does not exist" ): self.cmd.params("TGTX", "PKT1") def test_params_complains_about_non_existant_packets(self): with self.assertRaisesRegex( - RuntimeError, f"Command packet 'TGT1 PKTX' does not exist" + RuntimeError, "Command packet 'TGT1 PKTX' does not exist" ): self.cmd.params("TGT1", "PKTX") @@ -128,13 +115,13 @@ def test_params_returns_all_items_from_packet_tgt1_pkt1(self): def test_packet_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Command target 'TGTX' does not exist" + RuntimeError, "Command target 'TGTX' does not exist" ): self.cmd.packet("tgtX", "pkt1") def test_packet_complains_about_non_existant_packets(self): with self.assertRaisesRegex( - RuntimeError, f"Command packet 'TGT1 PKTX' does not exist" + RuntimeError, "Command packet 'TGT1 PKTX' does not exist" ): self.cmd.packet("TGT1", "PKTX") @@ -177,7 +164,7 @@ def test_identify_returns_None_with_unknown_targets_given(self): buffer = b"\x01\x02\x03\x04" self.assertIsNone(self.cmd.identify(buffer, ["TGTX"])) - def test_identify_logs_an_invalid_sized_buffer(self): + def test_identify_logs_an_invalid_sized_buffer1(self): for stdout in capture_io(): buffer = b"\x01\x02\x03" pkt = self.cmd.identify(buffer) @@ -190,7 +177,7 @@ def test_identify_logs_an_invalid_sized_buffer(self): stdout.getvalue(), ) - def test_identify_logs_an_invalid_sized_buffer(self): + def test_identify_logs_an_invalid_sized_buffer2(self): for stdout in capture_io(): buffer = b"\x01\x02\x03\x04\x05" pkt = self.cmd.identify(buffer) @@ -232,19 +219,19 @@ def test_identifies_tgt2_pkt1(self): def test_build_cmd_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Command target 'TGTX' does not exist" + RuntimeError, "Command target 'TGTX' does not exist" ): self.cmd.build_cmd("tgtX", "pkt1") def test_build_cmd_complains_about_non_existant_packets(self): with self.assertRaisesRegex( - RuntimeError, f"Command packet 'TGT1 PKTX' does not exist" + RuntimeError, "Command packet 'TGT1 PKTX' does not exist" ): self.cmd.build_cmd("tgt1", "pktX") def test_build_cmd_complains_about_non_existant_items(self): with self.assertRaisesRegex( - AttributeError, f"Packet item 'TGT1 PKT1 ITEMX' does not exist" + AttributeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist" ): self.cmd.build_cmd("tgt1", "pkt1", {"itemX": 1}) @@ -258,7 +245,7 @@ def test_build_cmd_creates_a_populated_command_packet_with_default_values(self): def test_build_cmd_complains_about_out_of_range_item_values(self): with self.assertRaisesRegex( RuntimeError, - f"Command parameter 'TGT1 PKT1 ITEM2' = 1000 not in valid range of 0 to 254", + "Command parameter 'TGT1 PKT1 ITEM2' = 1000 not in valid range of 0 to 254", ): self.cmd.build_cmd("tgt1", "pkt1", {"item2": 1000}) @@ -286,7 +273,7 @@ def test_build_cmd_creates_a_command_packet_with_override_item_value_states(self def test_build_cmd_complains_about_missing_required_parameters(self): with self.assertRaisesRegex( - RuntimeError, f"Required command parameter 'TGT2 PKT3 ITEM2' not given" + RuntimeError, "Required command parameter 'TGT2 PKT3 ITEM2' not given" ): self.cmd.build_cmd("tgt2", "pkt3") @@ -349,19 +336,19 @@ def test_format_ignores_parameters(self): def test_cmd_hazardous_complains_about_non_existant_targets(self): with self.assertRaisesRegex( - RuntimeError, f"Command target 'TGTX' does not exist" + RuntimeError, "Command target 'TGTX' does not exist" ): self.cmd.cmd_hazardous("tgtX", "pkt1") def test_cmd_hazardous_complains_about_non_existant_packets(self): with self.assertRaisesRegex( - RuntimeError, f"Command packet 'TGT1 PKTX' does not exist" + RuntimeError, "Command packet 'TGT1 PKTX' does not exist" ): self.cmd.cmd_hazardous("tgt1", "pktX") def test_cmd_hazardous_complains_about_non_existant_items(self): with self.assertRaisesRegex( - AttributeError, f"Packet item 'TGT1 PKT1 ITEMX' does not exist" + AttributeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist" ): self.cmd.cmd_hazardous("tgt1", "pkt1", {"itemX": 1}) diff --git a/openc3/python/test/test_helper.py b/openc3/python/test/test_helper.py index f73ea28046..3873a438d9 100644 --- a/openc3/python/test/test_helper.py +++ b/openc3/python/test/test_helper.py @@ -74,6 +74,14 @@ def setup_system(targets=["SYSTEM", "INST", "EMPTY"]): except RuntimeError: pass + try: + sets = {} + for set in System.limits.sets(): + sets[set] = "false" + Store.hset("DEFAULT__limits_sets", mapping=sets) + except RuntimeError: + pass + def mock_redis(self): # Ensure the store builds a new instance of redis and doesn't From 455ee05ba86d2754a7b3573cba4402fe8e99b2bb Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Mon, 16 Oct 2023 20:02:27 -0600 Subject: [PATCH 14/17] python tests --- .../python/test/interfaces/protocols/test_template_protocol.py | 1 + openc3/python/test/logs/test_stream_log.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openc3/python/test/interfaces/protocols/test_template_protocol.py b/openc3/python/test/interfaces/protocols/test_template_protocol.py index d5103bd3a8..5102a59b70 100644 --- a/openc3/python/test/interfaces/protocols/test_template_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_template_protocol.py @@ -53,6 +53,7 @@ def connected(self): return True def setUp(self): + mock_redis(self) TestTemplateProtocol.read_buffer = None TestTemplateProtocol.write_buffer = None self.interface = TestTemplateProtocol.MyInterface() diff --git a/openc3/python/test/logs/test_stream_log.py b/openc3/python/test/logs/test_stream_log.py index 0352a05c65..d5b5721d3e 100644 --- a/openc3/python/test/logs/test_stream_log.py +++ b/openc3/python/test/logs/test_stream_log.py @@ -24,6 +24,7 @@ class TestStreamLog(unittest.TestCase): def setUp(self): + mock_redis(self) self.mock = mock_s3(self) self.mock.clear() From 5b59239b6a2e9cc608cc349afc6ed5212d81e0fd Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Tue, 17 Oct 2023 08:42:50 -0600 Subject: [PATCH 15/17] Extend playwright timeout --- openc3/spec/api/cmd_api_spec.rb | 6 +++--- playwright/playwright.config.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openc3/spec/api/cmd_api_spec.rb b/openc3/spec/api/cmd_api_spec.rb index 20109c7770..ded35700f2 100644 --- a/openc3/spec/api/cmd_api_spec.rb +++ b/openc3/spec/api/cmd_api_spec.rb @@ -78,7 +78,7 @@ class ApiTest end @int_thread = Thread.new { @thread.run } - sleep 0.001 # Allow thread to spin up + sleep 0.01 # Allow thread to spin up @api = ApiTest.new end @@ -226,12 +226,12 @@ def test_cmd_unknown(method) model.create @dm = DecomMicroservice.new("DEFAULT__DECOM__INST_INT") @dm_thread = Thread.new { @dm.run } - sleep(0.001) + sleep(0.01) end after(:each) do @dm.shutdown - sleep(0.001) + sleep(0.01) end it "complains about unknown targets" do diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index 9e13377f14..d8cd9b5aff 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -15,7 +15,7 @@ export const ADMIN_STORAGE_STATE = path.join( export default defineConfig({ testDir: './tests', /* Maximum time one test can run for. */ - timeout: 3 * 60 * 1000, // 3 minutes + timeout: 5 * 60 * 1000, // 5 minutes expect: { /** * Maximum time expect() should wait for the condition to be met. From 4b69f79622f9ff5edddc38921e77f1d37c19e77b Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Tue, 17 Oct 2023 10:04:18 -0600 Subject: [PATCH 16/17] Increase timeouts --- openc3/python/test/api/test_interface_api.py | 2 +- playwright/tests/data-viewer.spec.ts | 5 ++++- playwright/tests/limits-monitor.spec.ts | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openc3/python/test/api/test_interface_api.py b/openc3/python/test/api/test_interface_api.py index d90447a9d0..a396e7046d 100644 --- a/openc3/python/test/api/test_interface_api.py +++ b/openc3/python/test/api/test_interface_api.py @@ -77,7 +77,7 @@ def build(): self.im = InterfaceMicroservice("DEFAULT__INTERFACE__INST_INT") self.im_thread = threading.Thread(target=self.im.run) self.im_thread.start() - time.sleep(0.001) # Allow the thread to run + time.sleep(0.01) # Allow the thread to run def tearDown(self): self.im.shutdown() diff --git a/playwright/tests/data-viewer.spec.ts b/playwright/tests/data-viewer.spec.ts index e05e56f0a7..bd93a7da8d 100644 --- a/playwright/tests/data-viewer.spec.ts +++ b/playwright/tests/data-viewer.spec.ts @@ -74,6 +74,7 @@ test('saves the configuration', async ({ page, utils }) => { }) test('opens and resets the configuration', async ({ page, utils }) => { + test.slow() // Open the config await page.locator('[data-test="cosmos-data-viewer-file"]').click() await page.locator('text=Open Configuration').click() @@ -82,7 +83,9 @@ test('opens and resets the configuration', async ({ page, utils }) => { await page.getByText('Loading configuration') try { await page.getByRole('button', { name: 'Dismiss' }).click() - } catch (error) {} + } catch (error) { + console.error(error) + } // Verify the config await page.getByRole('tab', { name: 'Test1' }).click() diff --git a/playwright/tests/limits-monitor.spec.ts b/playwright/tests/limits-monitor.spec.ts index 46cc3db521..830c4fe720 100644 --- a/playwright/tests/limits-monitor.spec.ts +++ b/playwright/tests/limits-monitor.spec.ts @@ -87,6 +87,7 @@ test('saves the configuration', async ({ page, utils }) => { }) test('opens and resets the configuration', async ({ page, utils }) => { + test.slow() await page.locator('[data-test=cosmos-limits-monitor-file]').click() await page.locator('text=Open Configuration').click() await page.locator(`td:has-text("playwright")`).click() @@ -94,7 +95,9 @@ test('opens and resets the configuration', async ({ page, utils }) => { await page.getByText('Loading configuration') try { await page.getByRole('button', { name: 'Dismiss' }).click() - } catch (error) {} + } catch (error) { + console.error(error) + } await page.locator('[data-test=cosmos-limits-monitor-file]').click() await page.locator('text=Show Ignored').click() From 317e98f8239ce45683f07920c255e99253ea75c4 Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Tue, 17 Oct 2023 10:58:03 -0600 Subject: [PATCH 17/17] test_cmd_api improvement --- openc3/python/test/api/test_cmd_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openc3/python/test/api/test_cmd_api.py b/openc3/python/test/api/test_cmd_api.py index ddc2401693..8ee16c1d2d 100644 --- a/openc3/python/test/api/test_cmd_api.py +++ b/openc3/python/test/api/test_cmd_api.py @@ -592,19 +592,19 @@ def test_get_cmd_time_returns_command_times(self): result = get_cmd_time("inst", "collect") self.assertEqual(result[0], ("INST")) self.assertEqual(result[1], ("COLLECT")) - self.assertEqual(result[2], int(now)) + self.assertAlmostEqual(result[2], int(now), delta=1) self.assertLess(abs(result[3] - int((now - int(now)) * 1_000_000)), 50000) result = get_cmd_time("INST") self.assertEqual(result[0], ("INST")) self.assertEqual(result[1], ("COLLECT")) - self.assertEqual(result[2], int(now)) + self.assertAlmostEqual(result[2], int(now), delta=1) self.assertLess(abs(result[3] - int((now - int(now)) * 1_000_000)), 50000) result = get_cmd_time() self.assertEqual(result[0], ("INST")) self.assertEqual(result[1], ("COLLECT")) - self.assertEqual(result[2], int(now)) + self.assertAlmostEqual(result[2], int(now), delta=1) self.assertLess(abs(result[3] - int((now - int(now)) * 1_000_000)), 50000) now = time.time() @@ -613,13 +613,13 @@ def test_get_cmd_time_returns_command_times(self): result = get_cmd_time("INST") self.assertEqual(result[0], ("INST")) self.assertEqual(result[1], ("ABORT")) # New latest is ABORT - self.assertEqual(result[2], int(now)) + self.assertAlmostEqual(result[2], int(now), delta=1) self.assertLess(abs(result[3] - int((now - int(now)) * 1_000_000)), 50000) result = get_cmd_time() self.assertEqual(result[0], ("INST")) self.assertEqual(result[1], ("ABORT")) - self.assertEqual(result[2], int(now)) + self.assertAlmostEqual(result[2], int(now), delta=1) self.assertLess(abs(result[3] - int((now - int(now)) * 1_000_000)), 50000) def test_get_cmd_time_returns_0_if_no_times_are_set(self):