diff --git a/openc3-cosmos-script-runner-api/scripts/running_script.py b/openc3-cosmos-script-runner-api/scripts/running_script.py index 3de1646bde..60803acbb5 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: @@ -1188,6 +1187,10 @@ def output_thread(self): openc3.script.RUNNING_SCRIPT = RunningScript +########################################################################### +# START PUBLIC API +########################################################################### + def step_mode(): RunningScript.instance.step() @@ -1275,9 +1278,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" @@ -1300,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 787876bb8f..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 @@ -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,21 +293,22 @@ def get_cmd_time(target_name = nil, command_name = nil, scope: $openc3_scope, to target_name = target_name.upcase targets = [target_name] end - 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) + time = 0 + command_name = nil + 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 - 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 +331,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 @@ -343,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) @@ -399,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/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/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/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/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 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/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/limits_api.py b/openc3/python/openc3/api/limits_api.py new file mode 100644 index 0000000000..d7713711ea --- /dev/null +++ b/openc3/python/openc3/api/limits_api.py @@ -0,0 +1,500 @@ +# 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. + +from datetime import datetime, timezone +from openc3.api import WHITELIST +from openc3.api.tlm_api import _tlm_process_args +from openc3.environment import OPENC3_SCOPE +from openc3.utilities.authorization import authorize +from openc3.topics.limits_event_topic import LimitsEventTopic +from openc3.models.cvt_model import CvtModel +from openc3.models.target_model import TargetModel + +# from openc3.utilities.extract import * +from openc3.utilities.logger import Logger +from openc3.utilities.time import to_nsec_from_epoch + +WHITELIST.extend( + [ + "get_out_of_limits", + "get_overall_limits_state", + "limits_enabled?", + "enable_limits", + "disable_limits", + "get_limits", + "set_limits", + "get_limits_groups", + "enable_limits_group", + "disable_limits_group", + "get_limits_sets", + "set_limits_set", + "get_limits_set", + "get_limits_events", + ] +) + + +# Return an array of arrays indicating all items in the packet that are out of limits +# [[target name, packet name, item name, item limits state], ...] +# +# @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." + ) + if item[2] is None: + item[2] = "" + 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 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" + 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) + sets = list(LimitsEventTopic.sets(scope=scope).keys()) + sets.sort() + return sets + + +# 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) + return 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/stash_api.py b/openc3/python/openc3/api/stash_api.py index b841985ff2..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 @@ -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/api/target_api.py b/openc3/python/openc3/api/target_api.py new file mode 100644 index 0000000000..a8cd8bde1f --- /dev/null +++ b/openc3/python/openc3/api/target_api.py @@ -0,0 +1,63 @@ +# 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.: + +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] 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 +105,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 +113,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 +139,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 +180,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 +196,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 +240,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 +255,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 +264,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 +291,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 @@ -344,56 +372,70 @@ 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" + mylist = [] + for k, v in result.items(): + mylist += [k, v] + return SUBSCRIPTION_DELIMITER.join(mylist) + + +# 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 + items = id.split(SUBSCRIPTION_DELIMITER) + # Convert it back into a dict to create a lookup + lookup = dict(zip(items[::2], items[1::2])) + 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 @@ -424,7 +466,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 +500,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 +517,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/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/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/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/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_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..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": @@ -635,8 +629,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/microservices/router_microservice.py b/openc3/python/openc3/microservices/router_microservice.py new file mode 100644 index 0000000000..a3bd4f20ae --- /dev/null +++ b/openc3/python/openc3/microservices/router_microservice.py @@ -0,0 +1,95 @@ +# 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 os +import sys +from openc3.microservices.interface_microservice import InterfaceMicroservice +from openc3.system.system import System +from openc3.models.router_status_model import RouterStatusModel +from openc3.topics.router_topic import RouterTopic + + +class RouterMicroservice(InterfaceMicroservice): + def handle_packet(self, packet): + RouterStatusModel.set(self.interface.as_json(), scope=self.scope) + if not packet.identified(): + # Need to identify so we can find the target + identified_packet = System.commands.identify( + packet.buffer_no_copy(), self.interface.cmd_target_names + ) + if identified_packet: + packet = identified_packet + + if not packet.defined(): + if packet.target_name and packet.packet_name: + try: + defined_packet = System.commands.packet( + packet.target_name, packet.packet_name + ) + defined_packet.received_time = packet.received_time + defined_packet.stored = packet.stored + defined_packet.buffer = packet.buffer + packet = defined_packet + except RuntimeError: + self.logger.warn(f"Error defining packet of {len(packet)} bytes") + + print(f"packet:{packet} tgt:{packet.target_name} pkt:{packet.packet_name}") + target_name = packet.target_name + if not target_name: + target_name = "UNKNOWN" + target = System.targets.get(target_name) + print(f"int:{self.interface} cmd:{self.interface.cmd_target_names}") + + try: + try: + log_message = True # Default is True + # If the packet has the DISABLE_MESSAGES keyword then no messages by default + if packet.messages_disabled: + log_message = False + # Check if any of the parameters have DISABLE_MESSAGES + for item in packet.sorted_items: + if item.states and item.messages_disabled: + value = packet.read_item(item) + if item.messages_disabled[value]: + log_message = False + break + + if log_message: + if target and target_name != "UNKNOWN": + self.logger.info( + System.commands.format(packet, target.ignored_parameters) + ) + else: + self.logger.warn( + f"Unidentified packet of {len(packet.buffer_no_copy())} bytes being routed to target {self.interface.cmd_target_names[0]}" + ) + except RuntimeError as error: + self.logger.error( + f"Problem formatting command from router=\n{repr(error)}" + ) + + RouterTopic.route_command( + packet, self.interface.cmd_target_names, scope=self.scope + ) + except RuntimeError as error: + self.error = error + self.logger.error( + f"Error routing command from {self.interface.name}\n{repr(error)}" + ) + + +if os.path.basename(__file__) == os.path.basename(sys.argv[0]): + RouterMicroservice.class_run() diff --git a/openc3/python/openc3/models/cvt_model.py b/openc3/python/openc3/models/cvt_model.py index b547df60ed..5819df1e9f 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,39 +268,68 @@ def normalize( hash = json.loads(hash) else: hash = {} - try: - match type: - case "ALL": - hash.pop(item_name) - hash.pop(f"{item_name}__C") - hash.pop(f"{item_name}__F") - hash.pop(f"{item_name}__U") - case "RAW": + match type: + case "ALL": + 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) - 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: + 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) ) + @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/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/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/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/models/target_model.py b/openc3/python/openc3/models/target_model.py index 805a6fe9b4..34569cae6c 100644 --- a/openc3/python/openc3/models/target_model.py +++ b/openc3/python/openc3/models/target_model.py @@ -15,8 +15,10 @@ # if purchased from OpenC3, Inc. import json +import time from openc3.models.model import Model from openc3.utilities.store import Store +from openc3.utilities.logger import Logger from openc3.environment import OPENC3_SCOPE @@ -30,6 +32,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 @@ -84,6 +88,25 @@ def packets(cls, target_name, type="TLM", scope=OPENC3_SCOPE): result.append(json.loads(packet_json)) return result + @classmethod + def set_packet( + cls, target_name, packet_name, packet, type="TLM", scope=OPENC3_SCOPE + ): + if type not in cls.VALID_TYPES: + raise RuntimeError(f"Unknown type {type} for {target_name} {packet_name}") + + try: + Store.hset( + f"{scope}__openc3{type.lower()}__{target_name}", + packet_name, + json.dumps(packet), + ) + except RuntimeError as error: + Logger.error( + f"Invalid text present in {target_name} {packet_name} {type.lower()} packet" + ) + raise error + @classmethod def packet_item( cls, target_name, packet_name, item_name, type="TLM", scope=OPENC3_SCOPE @@ -101,6 +124,65 @@ 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 + + # @return [Hash{String => 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: + 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/commands.py b/openc3/python/openc3/packets/commands.py index f9c7f1588e..3ee3376c37 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 @@ -55,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 @@ -66,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" ) @@ -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() @@ -105,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 @@ -220,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) @@ -259,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/limits.py b/openc3/python/openc3/packets/limits.py index 2a8a7cc359..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. # @@ -28,8 +26,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 +50,152 @@ 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", ) - """ - 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/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/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/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 31a8770830..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. # @@ -208,7 +206,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 +223,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 +235,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 +250,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/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/system/system.py b/openc3/python/openc3/system/system.py index f92f5fe4bb..0298691b6e 100644 --- a/openc3/python/openc3/system/system.py +++ b/openc3/python/openc3/system/system.py @@ -103,7 +103,7 @@ def __init__(self, target_names, target_config_dir): System.packet_config = PacketConfig() System.commands = Commands(System.packet_config, System) System.telemetry = Telemetry(System.packet_config, System) - System.limits = Limits(System.packet_config) + System.limits = Limits(System.packet_config, System) for target_name in target_names: self.add_target(target_name, target_config_dir) 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 cba2b79560..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 @@ -40,7 +41,7 @@ def write(cls, event, scope): case "LIMITS_SETTINGS": # Limits updated in limits_api.rb to avoid circular reference to TargetModel - if not cls.sets(scope=scope).has_key(event["limits_set"]): + if not cls.sets(scope=scope).get(event["limits_set"]): Store.hset(f"{scope}__limits_sets", event["limits_set"], "false") field = f"{event['target_name']}__{event['packet_name']}__{event['item_name']}" @@ -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/openc3/topics/router_topic.py b/openc3/python/openc3/topics/router_topic.py index d12564c9d3..111f7cc06f 100644 --- a/openc3/python/openc3/topics/router_topic.py +++ b/openc3/python/openc3/topics/router_topic.py @@ -14,26 +14,30 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. +import json +import time from openc3.topics.topic import Topic from openc3.system.system import System +from openc3.utilities.json import JsonEncoder +from openc3.environment import OPENC3_SCOPE class RouterTopic(Topic): # Generate a list of topics for this router. This includes the router itself # and all the targets which are assigned to this router. @classmethod - def topics(cls, router, scope): + def topics(cls, router, scope=OPENC3_SCOPE): topics = [] topics.append(f"{{{scope}__CMD}}ROUTER__{router.name}") for target_name in router.tlm_target_names: - for packet_name, packet in System.telemetry.packets(target_name): + for _, packet in System.telemetry.packets(target_name).items(): topics.append( f"{scope}__TELEMETRY__{{{packet.target_name}}}__{packet.packet_name}" ) return topics @classmethod - def receive_telemetry(cls, router, scope): + def receive_telemetry(cls, router, scope=OPENC3_SCOPE): while True: for topic, msg_id, msg_hash, redis in Topic.read_topics( RouterTopic.topics(router, scope) @@ -44,3 +48,115 @@ def receive_telemetry(cls, router, scope): ack_topic[1] = "ACK" + ack_topic[1] ack_topic = "__".join(ack_topic) Topic.write_topic(ack_topic, {"result": result}, msg_id, 100) + + @classmethod + def route_command(cls, packet, target_names, scope=OPENC3_SCOPE): + if packet.identified(): + topic = f"{{{scope}__CMD}}TARGET__{packet.target_name}" + Topic.write_topic( + topic, + { + "target_name": packet.target_name, + "cmd_name": packet.packet_name, + "cmd_buffer": json.dumps(packet.buffer_no_copy(), cls=JsonEncoder), + }, + "*", + 100, + ) + elif len(target_names) == 1: + topic = f"{{{scope}__CMD}}TARGET__{target_names[0]}" + target_name = "UNKNOWN" + if packet.target_name is not None: + target_name = packet.target_name + Topic.write_topic( + topic, + { + "target_name": target_name, + "cmd_name": "UNKNOWN", + "cmd_buffer": json.dumps(packet.buffer_no_copy(), cls=JsonEncoder), + }, + "*", + 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 + raise RuntimeError(f"No route for command: {target_name} {packet_name}") + + @classmethod + def connect_router(cls, router_name, *router_params, scope=OPENC3_SCOPE): + if router_params and len(router_params) == 0: + Topic.write_topic( + f"{{{scope}__CMD}}ROUTER__{router_name}", + {"connect": "True", "params": json.dumps(router_params)}, + "*", + 100, + ) + else: + Topic.write_topic( + f"{{{scope}__CMD}}ROUTER__{router_name}", {"connect": "True"}, "*", 100 + ) + + @classmethod + def disconnect_router(cls, router_name, scope=OPENC3_SCOPE): + Topic.write_topic( + f"{{{scope}__CMD}}ROUTER__{router_name}", {"disconnect": "True"}, "*", 100 + ) + + @classmethod + def start_raw_logging(cls, router_name, scope=OPENC3_SCOPE): + Topic.write_topic( + f"{{{scope}__CMD}}ROUTER__{router_name}", {"log_stream": "True"}, "*", 100 + ) + + @classmethod + def stop_raw_logging(cls, router_name, scope=OPENC3_SCOPE): + Topic.write_topic( + f"{{{scope}__CMD}}ROUTER__{router_name}", {"log_stream": "False"}, "*", 100 + ) + + @classmethod + def shutdown(cls, router, scope=OPENC3_SCOPE): + Topic.write_topic( + f"{{{scope}__CMD}}ROUTER__{router.name}", {"shutdown": "True"}, "*", 100 + ) + time.sleep(1) # Give some time for the interface to shutdown + RouterTopic.clear_topics(RouterTopic.topics(router, scope=scope)) + + @classmethod + def router_cmd(cls, router_name, cmd_name, *cmd_params, scope=OPENC3_SCOPE): + data = {} + data["cmd_name"] = cmd_name + data["cmd_params"] = cmd_params + Topic.write_topic( + f"{{{scope}__CMD}}ROUTER__{router_name}", + {"router_cmd": json.dumps(data)}, + "*", + 100, + ) + + @classmethod + def protocol_cmd( + cls, + router_name, + cmd_name, + *cmd_params, + read_write="READ_WRITE", + index=-1, + scope=OPENC3_SCOPE, + ): + data = {} + data["cmd_name"] = cmd_name + data["cmd_params"] = cmd_params + data["read_write"] = str(read_write).upper() + data["index"] = index + Topic.write_topic( + f"{{{scope}__CMD}}ROUTER__{router_name}", + {"protocol_cmd": json.dumps(data)}, + "*", + 100, + ) 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/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 c87f5fcfa6..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 @@ -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/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_cmd_api.py b/openc3/python/test/api/test_cmd_api.py index dd899edbb3..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,19 +613,19 @@ 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): - 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"): 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..a396e7046d --- /dev/null +++ b/openc3/python/test/api/test_interface_api.py @@ -0,0 +1,220 @@ +# 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() + @staticmethod + 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() + self.im = InterfaceMicroservice("DEFAULT__INTERFACE__INST_INT") + self.im_thread = threading.Thread(target=self.im.run) + self.im_thread.start() + time.sleep(0.01) # 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_limits_api.py b/openc3/python/test/api/test_limits_api.py new file mode 100644 index 0000000000..8d57945d0f --- /dev/null +++ b/openc3/python/test/api/test_limits_api.py @@ -0,0 +1,493 @@ +# 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 +from datetime import datetime, timezone +import unittest +import threading +from unittest.mock import * +from test.test_helper import * +from openc3.api.limits_api import * +from openc3.api.tlm_api import * +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 TestLimitsApi(unittest.TestCase): + @patch("openc3.microservices.microservice.System") + def setUp(self, system): + redis = mock_redis(self) + setup_system() + + orig_xread = redis.xread + + # 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, **kwargs) + 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)) + + model = TargetModel(name="INST", scope="DEFAULT") + model.create() + model = TargetModel(name="SYSTEM", scope="DEFAULT") + model.create() + model = MicroserviceModel( + name="DEFAULT__DECOM__INST_INT", + scope="DEFAULT", + topics=["DEFAULT__TELEMETRY__{INST}__HEALTH_STATUS"], + 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) # Allow the threads to run + + def tearDown(self): + self.dm.shutdown() + time.sleep(0.001) + + def test_get_limits_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + 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, "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, "Item 'INST HEALTH_STATUS BLAH' does not exist" + ): + get_limits("INST", "HEALTH_STATUS", "BLAH") + + def test_gets_limits_for_an_item(self): + self.assertEqual( + get_limits("INST", "HEALTH_STATUS", "TEMP1"), + { + "DEFAULT": [-80.0, -70.0, 60.0, 80.0, -20.0, 20.0], + "TVAC": [-80.0, -30.0, 30.0, 80.0], + }, + ) + + def test_gets_limits_for_a_latest_item(self): + 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 + packet.received_time = datetime.now(timezone.utc) + packet.check_limits() + TelemetryDecomTopic.write_packet(packet, scope="DEFAULT") + time.sleep(0.01) # Allow the write to happen + + self.assertEqual( + get_limits("INST", "LATEST", "TEMP1"), + { + "DEFAULT": [-80.0, -70.0, 60.0, 80.0, -20.0, 20.0], + "TVAC": [-80.0, -30.0, 30.0, 80.0], + }, + ) + + def test_set_limits_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + 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, "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, "Item 'INST HEALTH_STATUS BLAH' does not exist" + ): + set_limits("INST", "HEALTH_STATUS", "BLAH", 0.0, 10.0, 20.0, 30.0) + + def test_set_limits_creates_a_custom_limits_set(self): + set_limits("INST", "HEALTH_STATUS", "TEMP1", 0.0, 10.0, 20.0, 30.0) + self.assertEqual( + get_limits("INST", "HEALTH_STATUS", "TEMP1")["CUSTOM"], + ([0.0, 10.0, 20.0, 30.0]), + ) + + def test_set_limits_complains_about_invalid_limits(self): + 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, "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): + item = get_item("INST", "HEALTH_STATUS", "TEMP1") + self.assertNotEqual(item["limits"]["persistence_setting"], 10) + self.assertTrue(item["limits"]["enabled"]) + set_limits( + "INST", + "HEALTH_STATUS", + "TEMP1", + 0.0, + 1.0, + 4.0, + 5.0, + 2.0, + 3.0, + "DEFAULT", + 10, + False, + ) + item = get_item("INST", "HEALTH_STATUS", "TEMP1") + self.assertEqual(item["limits"]["persistence_setting"], (10)) + self.assertIsNone(item["limits"].get("enabled")) + self.assertEqual( + item["limits"]["DEFAULT"], + { + "red_low": 0.0, + "yellow_low": 1.0, + "yellow_high": 4.0, + "red_high": 5.0, + "green_low": 2.0, + "green_high": 3.0, + }, + ) + # Verify it also works with symbols for the set + set_limits( + "INST", + "HEALTH_STATUS", + "TEMP1", + 1.0, + 2.0, + 5.0, + 6.0, + 3.0, + 4.0, + "DEFAULT", + 10, + False, + ) + item = get_item("INST", "HEALTH_STATUS", "TEMP1") + self.assertEqual(item["limits"]["persistence_setting"], (10)) + self.assertIsNone(item["limits"].get("enabled")) + self.assertEqual( + item["limits"]["DEFAULT"], + { + "red_low": 1.0, + "yellow_low": 2.0, + "yellow_high": 5.0, + "red_high": 6.0, + "green_low": 3.0, + "green_high": 4.0, + }, + ) + + def test_get_limits_groups_returns_all_the_limits_groups(self): + self.assertEqual( + get_limits_groups(), + { + "FIRST": [ + ["INST", "HEALTH_STATUS", "TEMP1"], + ["INST", "HEALTH_STATUS", "TEMP3"], + ], + "SECOND": [ + ["INST", "HEALTH_STATUS", "TEMP2"], + ["INST", "HEALTH_STATUS", "TEMP4"], + ], + }, + ) + + def test_enable_limits_groups_complains_about_undefined_limits_groups(self): + with self.assertRaisesRegex( + RuntimeError, + "LIMITS_GROUP MINE undefined. Ensure your telemetry definition contains the line: LIMITS_GROUP MINE", + ): + enable_limits_group("MINE") + + def test_enable_limits_groups_enables_limits_for_all_items_in_the_group(self): + disable_limits("INST", "HEALTH_STATUS", "TEMP1") + disable_limits("INST", "HEALTH_STATUS", "TEMP3") + self.assertFalse(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + self.assertFalse(limits_enabled("INST", "HEALTH_STATUS", "TEMP3")) + enable_limits_group("FIRST") + self.assertTrue(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + self.assertTrue(limits_enabled("INST", "HEALTH_STATUS", "TEMP3")) + + def test_disable_limits_groups_complains_about_undefined_limits_groups(self): + with self.assertRaisesRegex( + RuntimeError, + "LIMITS_GROUP MINE undefined. Ensure your telemetry definition contains the line: LIMITS_GROUP MINE", + ): + disable_limits_group("MINE") + + def test_disable_limits_groups_disables_limits_for_all_items_in_the_group(self): + enable_limits("INST", "HEALTH_STATUS", "TEMP1") + enable_limits("INST", "HEALTH_STATUS", "TEMP3") + self.assertTrue(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + self.assertTrue(limits_enabled("INST", "HEALTH_STATUS", "TEMP3")) + disable_limits_group("FIRST") + 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_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( + "INST", + "HEALTH_STATUS", + {"TEMP1": 0, "TEMP2": 0, "TEMP3": 0, "TEMP4": 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)) + + items = get_out_of_limits() + for i in range(0, 4): + self.assertEqual(items[i][0], "INST") + self.assertEqual(items[i][1], "HEALTH_STATUS") + self.assertEqual(items[i][2], f"TEMP{i + 1}") + self.assertEqual(items[i][3], "RED_LOW") + + def test_get_overall_limits_state_returns_the_overall_system_limits_state(self): + inject_tlm( + "INST", + "HEALTH_STATUS", + { + "TEMP1": 0, + "TEMP2": 0, + "TEMP3": 0, + "TEMP4": 0, + "GROUND1STATUS": 1, + "GROUND2STATUS": 1, + }, + ) + 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.1) + self.assertEqual(get_overall_limits_state(), "YELLOW") + inject_tlm( + "INST", "HEALTH_STATUS", {"TEMP1": -75, "TEMP2": 40, "TEMP3": 0, "TEMP4": 0} + ) + time.sleep(0.1) + self.assertEqual(get_overall_limits_state(), "RED") + self.assertEqual(get_overall_limits_state([]), "RED") + + # Ignoring all now yields GREEN + self.assertEqual( + get_overall_limits_state([["INST", "HEALTH_STATUS", None]]), "GREEN" + ) + # Ignoring just TEMP2 yields YELLOW due to TEMP1 + self.assertEqual( + get_overall_limits_state([["INST", "HEALTH_STATUS", "TEMP2"]]), "YELLOW" + ) + + 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, "HEALTH_STATUS"): + get_overall_limits_state([["INST", "HEALTH_STATUS"]]) + + def test_limits_enabled_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + 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, "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, "Item 'INST HEALTH_STATUS BLAH' does not exist" + ): + limits_enabled("INST", "HEALTH_STATUS", "BLAH") + + def test_limits_enabled_returns_whether_limits_are_enable_for_an_item(self): + self.assertTrue(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + + def test_enable_limits_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + 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, "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, "Item 'INST HEALTH_STATUS BLAH' does not exist" + ): + enable_limits("INST", "HEALTH_STATUS", "BLAH") + + def test_enable_limits_enables_limits_for_an_item(self): + self.assertTrue(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + disable_limits("INST", "HEALTH_STATUS", "TEMP1") + self.assertFalse(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + enable_limits("INST", "HEALTH_STATUS", "TEMP1") + self.assertTrue(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + + def test_disable_limits_complains_about_non_existant_targets(self): + with self.assertRaisesRegex( + 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, "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, "Item 'INST HEALTH_STATUS BLAH' does not exist" + ): + disable_limits("INST", "HEALTH_STATUS", "BLAH") + + def test_disable_limits_disables_limits_for_an_item(self): + self.assertTrue(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + disable_limits("INST", "HEALTH_STATUS", "TEMP1") + self.assertFalse(limits_enabled("INST", "HEALTH_STATUS", "TEMP1")) + enable_limits("INST", "HEALTH_STATUS", "TEMP1") diff --git a/openc3/python/test/api/test_router_api.py b/openc3/python/test/api/test_router_api.py new file mode 100644 index 0000000000..0b02d2df99 --- /dev/null +++ b/openc3/python/test/api/test_router_api.py @@ -0,0 +1,185 @@ +# 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 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 + + +class TestRouterApi(unittest.TestCase): + router_cmd_data = {} + protocol_cmd_data = {} + + @patch("openc3.models.router_model.RouterModel.get_model") + @patch("openc3.microservices.interface_microservice.System") + def setUp(self, mock_system, mock_get_model): + mock_redis(self) + setup_system() + + class MyInterface(Interface): + def __init__(self): + super().__init__() + self.target_names = ["INST"] + self.cmd_target_names = ["INST"] + self.tlm_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): + TestRouterApi.router_cmd_data[cmd_name] = cmd_params + + def protocol_cmd( + self, cmd_name, *cmd_params, read_write="READ_WRITE", index=-1 + ): + TestRouterApi.protocol_cmd_data[cmd_name] = cmd_params + + # Allow the stubbed RouterModel.get_model to call build() + @staticmethod + def build(): + return MyInterface() + + mock_get_model.return_value = MyInterface + + model = RouterModel( + name="ROUTE_INT", + scope="DEFAULT", + target_names=["INST"], + cmd_target_names=["INST"], + tlm_target_names=["INST"], + config_params=["openc3/interfaces/interface.py"], + ) + model.create() + # self.im = RouterMicroservice("DEFAULT__INTERFACE__ROUTE_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_router_hash(self): + # interface = get_router("ROUTE_INT") + # self.assertEqual(type(interface), dict) + # self.assertEqual(interface["name"], "ROUTE_INT") + # # Verify it also includes the status + # self.assertEqual(interface["state"], "CONNECTED") + # self.assertEqual(interface["clients"], 0) + + def test_returns_all_router_names(self): + model = RouterModel(name="INT1", scope="DEFAULT") + model.create() + model = RouterModel(name="INT2", scope="DEFAULT") + model.create() + self.assertEqual(get_router_names(), ["INT1", "INT2", "ROUTE_INT"]) + + # def test_connects_the_router(self): + # self.assertEqual(get_router("ROUTE_INT")["state"], "CONNECTED") + # disconnect_router("ROUTE_INT") + # time.sleep(0.1) + # self.assertEqual(get_router("ROUTE_INT")["state"], "DISCONNECTED") + # connect_router("ROUTE_INT") + # time.sleep(0.1) + # self.assertIn(get_router("ROUTE_INT")["state"], ["ATTEMPTING", "CONNECTED"]) + + # def test_should_start_and_stop_raw_logging_on_the_router(self): + # self.assertIsNone(self.im.interface.stream_log_pair) + # start_raw_logging_router("ROUTE_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_router("ROUTE_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_router("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_router("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_router_name_and_all_info(self): + # info = get_all_router_info() + # self.assertEqual(info[0][0], "ROUTE_INT") + # self.assertEqual(info[0][1], "CONNECTED") + + # def test_sends_an_router_cmd(self): + # TestRouterApi.router_cmd_data = {} + # router_cmd("ROUTE_INT", "cmd1") + # time.sleep(0.1) + # self.assertEqual(list(TestRouterApi.router_cmd_data.keys()), ["cmd1"]) + # self.assertEqual(TestRouterApi.router_cmd_data["cmd1"], ()) + + # TestRouterApi.router_cmd_data = {} + # router_cmd("ROUTE_INT", "cmd2", "param1") + # time.sleep(0.1) + # self.assertEqual(list(TestRouterApi.router_cmd_data.keys()), ["cmd2"]) + # self.assertEqual(TestRouterApi.router_cmd_data["cmd2"], ("param1",)) + + # TestRouterApi.router_cmd_data = {} + # router_cmd("ROUTE_INT", "cmd3", "param1", "param2") + # time.sleep(0.1) + # self.assertEqual(list(TestRouterApi.router_cmd_data.keys()), ["cmd3"]) + # self.assertEqual( + # TestRouterApi.router_cmd_data["cmd3"], + # ( + # "param1", + # "param2", + # ), + # ) + + # def test_sends_a_protocol_cmd(self): + # TestRouterApi.protocol_cmd_data = {} + # router_protocol_cmd("ROUTE_INT", "cmd1") + # time.sleep(0.1) + # self.assertEqual(list(TestRouterApi.protocol_cmd_data.keys()), ["cmd1"]) + # self.assertEqual(TestRouterApi.protocol_cmd_data["cmd1"], ()) + + # TestRouterApi.protocol_cmd_data = {} + # router_protocol_cmd("ROUTE_INT", "cmd2", "param1") + # time.sleep(0.1) + # self.assertEqual(list(TestRouterApi.protocol_cmd_data.keys()), ["cmd2"]) + # self.assertEqual(TestRouterApi.protocol_cmd_data["cmd2"], ("param1",)) + + # TestRouterApi.protocol_cmd_data = {} + # router_protocol_cmd("ROUTE_INT", "cmd3", "param1", "param2") + # time.sleep(0.1) + # self.assertEqual(list(TestRouterApi.protocol_cmd_data.keys()), ["cmd3"]) + # self.assertEqual( + # TestRouterApi.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_target_api.py b/openc3/python/test/api/test_target_api.py new file mode 100644 index 0000000000..f78a3a6973 --- /dev/null +++ b/openc3/python/test/api/test_target_api.py @@ -0,0 +1,65 @@ +# 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.target_api import * +from openc3.models.target_model import TargetModel +from openc3.models.interface_model import InterfaceModel + + +class TestTargetApi(unittest.TestCase): + def setUp(self): + mock_redis(self) + setup_system() + + model = InterfaceModel( + name="INST_INT", + scope="DEFAULT", + target_names=["INST"], + config_params=["openc3/interfaces/interface.py"], + ) + model.create() + model = TargetModel(folder_name="INST", name="INST", scope="DEFAULT") + model.create() + model = TargetModel(folder_name="EMPTY", name="EMPTY", scope="DEFAULT") + model.create() + model = TargetModel(folder_name="SYSTEM", name="SYSTEM", scope="DEFAULT") + model.create() + + def test_gets_an_empty_array_for_an_unknown_scope(self): + self.assertEqual(len(get_target_names(scope="UNKNOWN")), 0) + + def test_gets_the_list_of_targets(self): + self.assertEqual(get_target_names(scope="DEFAULT"), ["EMPTY", "INST", "SYSTEM"]) + + def test_returns_none_if_the_target_doesnt_exist(self): + self.assertIsNone(get_target("BLAH", scope="DEFAULT")) + + def test_gets_a_target_hash(self): + tgt = get_target("INST", scope="DEFAULT") + self.assertEqual(type(tgt), dict) + self.assertEqual(tgt["name"], "INST") + + def test_gets_target_name_interface_names(self): + info = get_target_interfaces(scope="DEFAULT") + self.assertEqual(info[0][0], "EMPTY") + self.assertEqual(info[0][1], "") + self.assertEqual(info[1][0], "INST") + self.assertEqual(info[1][1], "INST_INT") + self.assertEqual(info[2][0], "SYSTEM") + self.assertEqual(info[2][1], "") diff --git a/openc3/python/test/api/test_tlm_api.py b/openc3/python/test/api/test_tlm_api.py index 5904a548b8..d18346dbb6 100644 --- a/openc3/python/test/api/test_tlm_api.py +++ b/openc3/python/test/api/test_tlm_api.py @@ -14,43 +14,966 @@ # 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, r"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.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) + 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 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"): + 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/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/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/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/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() 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..a120f98c78 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" ) @@ -94,8 +93,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", ) @@ -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() @@ -356,7 +355,6 @@ def test_gettlm_raises_on_invalid_types(self): CvtModel.get_tlm_values([["INST", "HEALTH_STATUS", "TEMP1", "NOPE"]]) def test_gets_different_value_types_from_the_cvt(self): - print("***test_gets_different_value_types_from_the_cvt***") self.update_temp1() values = [ ["INST", "HEALTH_STATUS", "TEMP1", "RAW"], @@ -375,7 +373,6 @@ def test_gets_different_value_types_from_the_cvt(self): self.assertEqual(result[3][1], "GREEN") def test_marks_values_stale(self): - print("***test_marks_values_stale***") self.update_temp1(rxtime=(time.time() - 10)) values = [["INST", "HEALTH_STATUS", "TEMP1", "RAW"]] result = CvtModel.get_tlm_values(values, stale_time=9) @@ -386,7 +383,6 @@ def test_marks_values_stale(self): self.assertEqual(result[0][1], "GREEN") def test_returns_overridden_values(self): - print("***test_returns_overridden_values***") self.update_temp1() json_hash = {} json_hash["DATA"] = "\x00\x01\x02" 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_commands.py b/openc3/python/test/packets/test_commands.py new file mode 100644 index 0000000000..be5001eb83 --- /dev/null +++ b/openc3/python/test/packets/test_commands.py @@ -0,0 +1,386 @@ +# 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 tempfile +import unittest +from unittest.mock import * +from test.test_helper import * +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 + + +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, "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, "Command target 'TGTX' does not exist" + ): + self.cmd.params("TGTX", "PKT1") + + def test_params_complains_about_non_existant_packets(self): + with self.assertRaisesRegex( + RuntimeError, "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, "Command target 'TGTX' does not exist" + ): + self.cmd.packet("tgtX", "pkt1") + + def test_packet_complains_about_non_existant_packets(self): + with self.assertRaisesRegex( + RuntimeError, "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_buffer1(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_buffer2(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, "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, "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, "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, + "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, "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, "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, "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, "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"]) diff --git a/openc3/python/test/packets/test_packet.py b/openc3/python/test/packets/test_packet.py index e0a5c1b9fa..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. # @@ -697,7 +695,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/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/streams/test_tcpip_socket_stream.py b/openc3/python/test/streams/test_tcpip_socket_stream.py index bc0c61cd57..ca73fe8d0b 100644 --- a/openc3/python/test/streams/test_tcpip_socket_stream.py +++ b/openc3/python/test/streams/test_tcpip_socket_stream.py @@ -26,6 +26,9 @@ class TestTcpipSocketStream(unittest.TestCase): + def setUp(self): + mock_redis(self) + def test_is_not_be_connected_when_initialized(self): ss = TcpipSocketStream(None, None, 10.0, None) self.assertFalse(ss.connected) 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_helper.py b/openc3/python/test/test_helper.py index a4046fe511..3873a438d9 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: @@ -61,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 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. # 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/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 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 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. 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()