diff --git a/pyproject.toml b/pyproject.toml index 32f2b868..fda627c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ classifiers = [ dependencies = [ "environs>=9.5.0", "django>=3.2.18,<4.0", - "its_preselector @ git+https://github.com/NTIA/Preselector@3.0.2", + "its_preselector @ git+https://github.com/NTIA/Preselector@3.1.0", "msgspec>=0.16.0,<1.0.0", "numexpr>=2.8.3", "numpy>=1.22.0", diff --git a/scos_actions/__init__.py b/scos_actions/__init__.py index 9c4b2035..b378fc6c 100644 --- a/scos_actions/__init__.py +++ b/scos_actions/__init__.py @@ -1 +1 @@ -__version__ = "6.4.2" +__version__ = "7.0.0" diff --git a/scos_actions/actions/acquire_sea_data_product.py b/scos_actions/actions/acquire_sea_data_product.py index 5fc636c5..c36caa94 100644 --- a/scos_actions/actions/acquire_sea_data_product.py +++ b/scos_actions/actions/acquire_sea_data_product.py @@ -20,6 +20,7 @@ Currently in development. """ +import json import logging import lzma import platform @@ -122,20 +123,6 @@ FFT_DETECTOR = create_statistical_detector("FftMeanMaxDetector", ["mean", "max"]) PFP_M3_DETECTOR = create_statistical_detector("PfpM3Detector", ["min", "max", "mean"]) -# Expected webswitch configuration: -PRESELECTOR_SENSORS = { - "temp": 1, # Internal temperature, deg C - "noise_diode_temp": 2, # Noise diode temperature, deg C - "lna_temp": 3, # LNA temperature, deg C - "humidity": 4, # Internal humidity, percentage -} -PRESELECTOR_DIGITAL_INPUTS = {"door_closed": 1} -SPU_SENSORS = { - "pwr_box_temp": 1, # Power tray temperature, deg C - "rf_box_temp": 2, # RF tray temperature, deg C - "pwr_box_humidity": 3, # Power tray humidity, percentage -} - @ray.remote class PowerSpectralDensity: @@ -704,47 +691,33 @@ def capture_diagnostics(self, action_start_tic: float, cpu_speeds: list) -> None consecutive points as the action has been running. """ tic = perf_counter() - # Read SPU sensors + switch_diag = {} + all_switch_status = {} + # Add status for any switch for switch in switches.values(): - if switch.name == "SPU X410": - spu_diag = switch.get_status() - del spu_diag["name"] - del spu_diag["healthy"] - for sensor in SPU_SENSORS: - try: - value = switch.get_sensor_value(SPU_SENSORS[sensor]) - spu_diag[sensor] = value - except: - logger.warning(f"Unable to read {sensor} from SPU x410") - try: - spu_diag["sigan_internal_temp"] = self.sigan.temperature - except: - logger.warning("Unable to read internal sigan temperature") - # Rename key for use with ** - spu_diag["aux_28v_powered"] = spu_diag.pop("28v_aux_powered") + switch_status = switch.get_status() + del switch_status["name"] + del switch_status["healthy"] + all_switch_status.update(switch_status) + + self.set_ups_states(all_switch_status, switch_diag) + self.add_temperature_and_humidity_sensors(all_switch_status, switch_diag) + self.add_power_sensors(all_switch_status, switch_diag) + self.add_power_states(all_switch_status, switch_diag) + if "door_state" in all_switch_status: + switch_diag["door_closed"] = not bool(all_switch_status["door_state"]) # Read preselector sensors - ps_diag = {} - for sensor in PRESELECTOR_SENSORS: - try: - value = preselector.get_sensor_value(PRESELECTOR_SENSORS[sensor]) - ps_diag[sensor] = value - except: - logger.warning(f"Unable to read {sensor} from preselector") - for inpt in PRESELECTOR_DIGITAL_INPUTS: - try: - value = preselector.get_digital_input_value( - PRESELECTOR_DIGITAL_INPUTS[inpt] - ) - ps_diag[inpt] = value - except: - logger.warning(f"Unable to read {inpt} from preselector") + ps_diag = preselector.get_status() + del ps_diag["name"] + del ps_diag["healthy"] # Read computer performance metrics cpu_diag = { # Start with CPU min/max/mean speeds "cpu_max_clock": round(max(cpu_speeds), 1), "cpu_min_clock": round(min(cpu_speeds), 1), "cpu_mean_clock": round(np.mean(cpu_speeds), 1), + "action_runtime": round(perf_counter() - action_start_tic, 2), } try: # Computer uptime (days) cpu_diag["cpu_uptime"] = round(get_cpu_uptime_seconds() / (60 * 60 * 24), 2) @@ -775,13 +748,13 @@ def capture_diagnostics(self, action_start_tic: float, cpu_speeds: list) -> None except: logger.warning("Failed to get CPU overheating status") try: # SCOS start time - cpu_diag["scos_start"] = convert_datetime_to_millisecond_iso_format( + cpu_diag["software_start"] = convert_datetime_to_millisecond_iso_format( start_time ) except: logger.warning("Failed to get SCOS start time") try: # SCOS uptime - cpu_diag["scos_uptime"] = get_days_up() + cpu_diag["software_uptime"] = get_days_up() except: logger.warning("Failed to get SCOS uptime") try: # SSD SMART data @@ -807,15 +780,159 @@ def capture_diagnostics(self, action_start_tic: float, cpu_speeds: list) -> None diagnostics = { "datetime": utils.get_datetime_str_now(), "preselector": ntia_diagnostics.Preselector(**ps_diag), - "spu": ntia_diagnostics.SPU(**spu_diag), + "spu": ntia_diagnostics.SPU(**switch_diag), "computer": ntia_diagnostics.Computer(**cpu_diag), "software": ntia_diagnostics.Software(**software_diag), - "action_runtime": round(perf_counter() - action_start_tic, 2), } # Add diagnostics to SigMF global object self.sigmf_builder.set_diagnostics(ntia_diagnostics.Diagnostics(**diagnostics)) + def set_ups_states(self, all_switch_status: dict, switch_diag: dict): + if "ups_power_state" in all_switch_status: + switch_diag["battery_backup"] = not all_switch_status["ups_power_state"] + else: + logger.warning("No ups_power_state found in switch status.") + + if "ups_battery_level" in all_switch_status: + switch_diag["low_battery"] = not all_switch_status["ups_battery_level"] + else: + logger.warning("No ups_battery_level found in switch status.") + + if "ups_state" in all_switch_status: + switch_diag["ups_healthy"] = not all_switch_status["ups_state"] + else: + logger.warning("No ups_state found in switch status.") + + if "ups_battery_state" in all_switch_status: + switch_diag["replace_battery"] = not all_switch_status["ups_battery_state"] + else: + logger.warning("No ups_battery_state found in switch status") + + def add_temperature_and_humidity_sensors( + self, all_switch_status: dict, switch_diag: dict + ): + switch_diag["temperature_sensors"] = [] + if "internal_temp" in all_switch_status: + switch_diag["temperature_sensors"].append( + {"name": "internal_temp", "value": all_switch_status["internal_temp"]} + ) + else: + logger.warning("No internal_temp found in switch status.") + try: + switch_diag["temperature_sensors"].append( + {"name": "sigan_internal_temp", "value": self.sigan.temperature} + ) + except: + logger.warning("Unable to read internal sigan temperature") + + if "tec_intake_temp" in all_switch_status: + switch_diag["temperature_sensors"].append( + { + "name": "tec_intake_temp", + "value": all_switch_status["tec_intake_temp"], + } + ) + else: + logger.warning("No tec_intake_temp found in switch status.") + + if "tec_exhaust_temp" in all_switch_status: + switch_diag["temperature_sensors"].append( + { + "name": "tec_exhaust_temp", + "value": all_switch_status["tec_exhaust_temp"], + } + ) + else: + logger.warning("No tec_exhaust_temp found in switch status.") + + if "internal_humidity" in all_switch_status: + switch_diag["humidity_sensors"] = [ + { + "name": "internal_humidity", + "value": all_switch_status["internal_humidity"], + } + ] + else: + logger.warning("No internal_humidity found in switch status.") + + def add_power_sensors(self, all_switch_status: dict, switch_diag: dict): + switch_diag["power_sensors"] = [] + if "power_monitor5V" in all_switch_status: + switch_diag["power_sensors"].append( + { + "name": "5V Monitor", + "value": all_switch_status["power_monitor5V"], + "expected_value": 5.0, + } + ) + else: + logger.warning("No power_monitor5V found in switch status") + + if "power_monitor15V" in all_switch_status: + switch_diag["power_sensors"].append( + { + "name": "15V Monitor", + "value": all_switch_status["power_monitor15V"], + "expected_value": 15.0, + } + ) + else: + logger.warning("No power_monitor15V found in switch status.") + + if "power_monitor24V" in all_switch_status: + switch_diag["power_sensors"].append( + { + "name": "24V Monitor", + "value": all_switch_status["power_monitor24V"], + "expected_value": 24.0, + } + ) + else: + logger.warning("No power_monitor24V found in switch status") + + if "power_monitor28V" in all_switch_status: + switch_diag["power_sensors"].append( + { + "name": "28V Monitor", + "value": all_switch_status["power_monitor28V"], + "expected_value": 28.0, + } + ) + else: + logger.warning("No power_monitor28V found in switch status") + + def add_heating_cooling(self, all_switch_status: dict, switch_diag: dict): + if "heating" in all_switch_status: + switch_diag["heating"] = all_switch_status["heating"] + else: + logger.warning("No heating found in switch status.") + + if "cooling" in all_switch_status: + switch_diag["cooling"] = all_switch_status["cooling"] + else: + logger.warning("No cooling found in switch status") + + def add_power_states(self, all_switch_status: dict, switch_diag: dict): + if "sigan_powered" in all_switch_status: + switch_diag["sigan_powered"] = all_switch_status["sigan_powered"] + else: + logger.warning("No sigan_powered found in switch status.") + + if "temperature_control_powered" in all_switch_status: + switch_diag["temperature_control_powered"] = all_switch_status[ + "temperature_control_powered" + ] + else: + logger.warning("No temperature_control_powered found in switch status.") + + if "preselector_powered" in all_switch_status: + switch_diag["preselector_powered"] = all_switch_status[ + "preselector_powered" + ] + else: + logger.warning("No preselector_powered found in switch status.") + def create_global_sensor_metadata(self): # Add (minimal) ntia-sensor metadata to the sigmf_builder: # sensor ID, serial numbers for preselector, sigan, and computer @@ -836,9 +953,6 @@ def test_required_components(self): msg = "Acquisition failed: signal analyzer is not available" trigger_api_restart.send(sender=self.__class__) raise RuntimeError(msg) - if "SPU X410" not in [s.name for s in switches.values()]: - msg = "Configuration error: no switch configured with name 'SPU X410'" - raise RuntimeError(msg) if not self.sigan.healthy(): trigger_api_restart.send(sender=self.__class__) return None diff --git a/scos_actions/metadata/sigmf_builder.py b/scos_actions/metadata/sigmf_builder.py index 7a3bb4a9..2a838d82 100644 --- a/scos_actions/metadata/sigmf_builder.py +++ b/scos_actions/metadata/sigmf_builder.py @@ -27,7 +27,7 @@ }, { "name": "ntia-diagnostics", - "version": "1.1.2", + "version": "2.0.0", "optional": True, }, { diff --git a/scos_actions/metadata/structs/ntia_diagnostics.py b/scos_actions/metadata/structs/ntia_diagnostics.py index 842d74c3..8c144299 100644 --- a/scos_actions/metadata/structs/ntia_diagnostics.py +++ b/scos_actions/metadata/structs/ntia_diagnostics.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional import msgspec @@ -11,43 +11,79 @@ class Preselector(msgspec.Struct, **SIGMF_OBJECT_KWARGS): :param temp: Temperature inside the preselector enclosure, in degrees Celsius. :param noise_diode_temp: Temperature of the noise diode, in degrees Celsius. + :param noise_diode_powered: Boolean indicating if the noise diode is powered. + :param lna_powered: Boolean indicating if the lna is powered. :param lna_temp: Temparature of the low noise amplifier, in degrees Celsius. + :param antenna_path_enabled: Boolean indicating if the antenna path is enabled. + :param noise_diode_path_enabled: Boolean indicating if the noise diode path is enabled. :param humidity: Relative humidity inside the preselector enclosure, as a percentage. :param door_closed: Indicates whether the door of the enclosure is closed. """ temp: Optional[float] = None noise_diode_temp: Optional[float] = None + noise_diode_powered: Optional[bool] = None + lna_powered: Optional[bool] = None lna_temp: Optional[float] = None + antenna_path_enabled: Optional[bool] = None + noise_diode_path_enabled: Optional[bool] = None humidity: Optional[float] = None door_closed: Optional[bool] = False -class SPU( - msgspec.Struct, rename={"aux_28v_powered": "28v_aux_powered"}, **SIGMF_OBJECT_KWARGS -): +class DiagnosticSensor(msgspec.Struct, **SIGMF_OBJECT_KWARGS): + """ + Interface for generating `ntia-diagnostics` `DiagnosticSensor` objects. + + :param name: The name of the sensor + :param value: The value provided by the sensor + :param maximum_allowed: The maximum value allowed from the sensor before action should be taken + :param mimimum_allowed: The minimum value allowed from the sensor before action should be taken + :param description: A description of the sensor + """ + + name: str + value: float + maximum_allowed: Optional[float] = None + minimum_allowed: Optional[float] = None + expected_value: Optional[float] = None + description: Optional[str] = None + + +class SPU(msgspec.Struct, **SIGMF_OBJECT_KWARGS): """ Interface for generating `ntia-diagnostics` `SPU` objects. - :param rf_tray_powered: Indicates if the RF tray is powered. + :param cooling: Boolean indicating if the cooling is enabled. + :param heating: Boolean indicating if the heat is enabled. :param preselector_powered: Indicates if the preselector is powered. - :param aux_28v_powered: Indicates if the 28V aux power is on. - :param pwr_box_temp: Ambient temperature in power distribution box, - in degrees Celsius. - :param pwr_box_humidity: Humidity in power distribution box, as a - percentage. - :param rf_box_temp: Ambient temperature in the RF box (around the signal - analyzer), in degrees Celsius. - :param sigan_internal_temp: Internal temperature reported by the signal analyzer. + :param sigan_powered: Boolean indicating if the signal analyzer is powered. + :param temperature_control_powered: Boolean indicating TEC AC power. + :param battery_backup: Boolean indicating if it is running on battery backup. + :param low_battery: Boolean indicating if the battery is low. + :param ups_healthy: Indicates trouble with UPS. + :param replace_battery: Boolean indicating if the ups battery needs replacing. + :param temperature_sensors: List of temperature sensor values + :param humidity_sensors: List of humidity sensor values + :param power_sensors: List of power sensor values + :param door_closed: Boolean indicating if the door is closed """ - rf_tray_powered: Optional[bool] = None + cooling: Optional[bool] = None + heating: Optional[bool] = None + sigan_powered: Optional[bool] = None + temperature_control_powered: Optional[bool] = None preselector_powered: Optional[bool] = None - aux_28v_powered: Optional[bool] = None - pwr_box_temp: Optional[float] = None - pwr_box_humidity: Optional[float] = None - rf_box_temp: Optional[float] = None - sigan_internal_temp: Optional[float] = None + + battery_backup: Optional[bool] = None + low_battery: Optional[bool] = None + ups_healthy: Optional[bool] = None + replace_battery: Optional[bool] = None + + temperature_sensors: Optional[List[DiagnosticSensor]] = None + humidity_sensors: Optional[List[DiagnosticSensor]] = None + power_sensors: Optional[List[DiagnosticSensor]] = None + door_closed: Optional[bool] = None class SsdSmartData(msgspec.Struct, **SIGMF_OBJECT_KWARGS): @@ -110,12 +146,13 @@ class Computer(msgspec.Struct, **SIGMF_OBJECT_KWARGS): cpu_mean_clock: Optional[float] = None cpu_uptime: Optional[float] = None action_cpu_usage: Optional[float] = None + action_runtime: Optional[float] = None system_load_5m: Optional[float] = None memory_usage: Optional[float] = None cpu_overheating: Optional[bool] = None cpu_temp: Optional[float] = None - scos_start: Optional[str] = None - scos_uptime: Optional[float] = None + software_start: Optional[str] = None + software_uptime: Optional[float] = None ssd_smart_data: Optional[SsdSmartData] = None @@ -169,4 +206,3 @@ class Diagnostics(msgspec.Struct, **SIGMF_OBJECT_KWARGS): spu: Optional[SPU] = None computer: Optional[Computer] = None software: Optional[Software] = None - action_runtime: Optional[float] = None