From 73af62a864033f502cc73812c787ec94fb0b5fe2 Mon Sep 17 00:00:00 2001 From: Nate Gay Date: Tue, 25 Mar 2025 23:09:13 -0500 Subject: [PATCH 1/8] Remove references to circuitpy_flight_software --- .vscode/settings.json | 2 +- README.md | 2 +- sonar-project.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e9fca2d..2c78b0b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,6 @@ "python.testing.pytestEnabled": true, "sonarlint.connectedMode.project": { "connectionId": "proves-kit", - "projectKey": "proveskit_circuitpy_flight_software" + "projectKey": "proveskit_circuitpython_rp2040_v5" } } diff --git a/README.md b/README.md index ff36b42..f07e0ab 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# circuitpy_flight_software +# ProvesKit RP2040 v4 CircuitPython Flight Software [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) ![CI](https://github.com/texas-state-space-lab/pikvm-tailscale-certificate-renewer/actions/workflows/ci.yaml/badge.svg) diff --git a/sonar-project.properties b/sonar-project.properties index b8bedef..35a7b00 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,4 @@ -sonar.projectKey=proveskit_circuitpy_flight_software +sonar.projectKey=proveskit_circuitpython_rp2040_v4 sonar.organization=proves-kit sonar.sources=boot.py, main.py, repl.py, safemode.py, lib/pysquared/ From 6989bd5ee598927e1d7636388f896cfcf46bcffd Mon Sep 17 00:00:00 2001 From: Nate Gay Date: Tue, 25 Mar 2025 23:32:32 -0500 Subject: [PATCH 2/8] Fix readme ci badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f07e0ab..09f6520 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ProvesKit RP2040 v4 CircuitPython Flight Software [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -![CI](https://github.com/texas-state-space-lab/pikvm-tailscale-certificate-renewer/actions/workflows/ci.yaml/badge.svg) +![CI](https://github.com/proveskit/CircuitPython_RP2040_v5/actions/workflows/ci.yaml/badge.svg) Software for CircuitPython flight software in the PROVES Kit. From 82361ba72ecdc01aef2b7f41fcddebfcdb603091 Mon Sep 17 00:00:00 2001 From: aychar <58487401+hrfarmer@users.noreply.github.com> Date: Wed, 26 Mar 2025 01:34:25 -0500 Subject: [PATCH 3/8] add function to install from local repo --- .gitignore | 1 + Makefile | 11 +- lib/pysquared/Big_Data.py | 2 +- lib/pysquared/cdh.py | 10 +- lib/pysquared/config/config.py | 47 --- lib/pysquared/config/radio.py | 25 -- lib/pysquared/functions.py | 22 +- lib/pysquared/hardware/__init__.py | 0 lib/pysquared/hardware/busio.py | 49 --- lib/pysquared/hardware/decorators.py | 36 --- lib/pysquared/hardware/digitalio.py | 35 --- lib/pysquared/hardware/exception.py | 2 - lib/pysquared/hardware/rfm9x/__init__.py | 0 lib/pysquared/hardware/rfm9x/factory.py | 172 ---------- lib/pysquared/hardware/rfm9x/manager.py | 105 ------- lib/pysquared/hardware/rfm9x/modulation.py | 5 - lib/pysquared/logger.py | 2 +- lib/pysquared/nvm/__init__.py | 3 - lib/pysquared/nvm/counter.py | 27 -- lib/pysquared/nvm/flag.py | 30 -- lib/pysquared/nvm/register.py | 6 - lib/pysquared/packet_manager.py | 2 +- lib/pysquared/packet_sender.py | 6 +- lib/pysquared/pysquared.py | 10 +- lib/pysquared/rtc/__init__.py | 0 lib/pysquared/rtc/rp2040.py | 38 --- lib/pysquared/rtc/rtc_common.py | 22 -- lib/pysquared/sleep_helper.py | 4 +- lib/pysquared/usb/usbfunctions.py | 348 --------------------- lib/requirements.txt | 1 + 30 files changed, 41 insertions(+), 980 deletions(-) mode change 100755 => 100644 lib/pysquared/Big_Data.py delete mode 100644 lib/pysquared/config/config.py delete mode 100644 lib/pysquared/config/radio.py mode change 100755 => 100644 lib/pysquared/functions.py delete mode 100644 lib/pysquared/hardware/__init__.py delete mode 100644 lib/pysquared/hardware/busio.py delete mode 100644 lib/pysquared/hardware/decorators.py delete mode 100644 lib/pysquared/hardware/digitalio.py delete mode 100644 lib/pysquared/hardware/exception.py delete mode 100644 lib/pysquared/hardware/rfm9x/__init__.py delete mode 100644 lib/pysquared/hardware/rfm9x/factory.py delete mode 100644 lib/pysquared/hardware/rfm9x/manager.py delete mode 100644 lib/pysquared/hardware/rfm9x/modulation.py delete mode 100644 lib/pysquared/nvm/__init__.py delete mode 100644 lib/pysquared/nvm/counter.py delete mode 100755 lib/pysquared/nvm/flag.py delete mode 100644 lib/pysquared/nvm/register.py delete mode 100644 lib/pysquared/rtc/__init__.py delete mode 100644 lib/pysquared/rtc/rp2040.py delete mode 100644 lib/pysquared/rtc/rtc_common.py delete mode 100644 lib/pysquared/usb/usbfunctions.py diff --git a/.gitignore b/.gitignore index 1d6407b..e90dc23 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ lib/rv3028* lib/adafruit* lib/asyncio* lib/neopixel.py +lib/pysquared/ diff --git a/Makefile b/Makefile index ac02bf9..43a81ff 100644 --- a/Makefile +++ b/Makefile @@ -13,10 +13,19 @@ help: ## Display this help. @$(UV) venv @$(UV) pip install --requirement pyproject.toml +LOCAL_PYSQUARED ?= "" + .PHONY: download-libraries download-libraries: .venv ## Download the required libraries @echo "Downloading libraries..." - @$(UV) pip install --requirement lib/requirements.txt --target lib --no-deps --upgrade --quiet + @$(UV) pip install --requirement lib/requirements.txt --target lib --no-deps --upgrade --quiet; \ + + @if [ -n "$(LOCAL_PYSQUARED)" ]; then \ + $(UV) pip install $(LOCAL_PYSQUARED) --target lib --no-deps --upgrade --quiet; \ + else \ + $(UV) pip install git+https://github.com/hrfarmer/pysquared --target lib --no-deps --upgrade --quiet; \ + fi + @rm -rf lib/*.dist-info @rm -rf lib/.lock diff --git a/lib/pysquared/Big_Data.py b/lib/pysquared/Big_Data.py old mode 100755 new mode 100644 index b648598..0031b63 --- a/lib/pysquared/Big_Data.py +++ b/lib/pysquared/Big_Data.py @@ -1,7 +1,7 @@ import gc import lib.adafruit_tca9548a as adafruit_tca9548a # I2C Multiplexer -from lib.pysquared.logger import Logger +from pysquared.logger import Logger try: from typing import Union diff --git a/lib/pysquared/cdh.py b/lib/pysquared/cdh.py index 1414e0d..4173752 100644 --- a/lib/pysquared/cdh.py +++ b/lib/pysquared/cdh.py @@ -1,11 +1,11 @@ import random import time -from lib.pysquared.config.config import Config -from lib.pysquared.hardware.rfm9x.manager import RFM9xManager -from lib.pysquared.hardware.rfm9x.modulation import RFM9xModulation -from lib.pysquared.logger import Logger -from lib.pysquared.pysquared import Satellite +from pysquared.config.config import Config +from pysquared.hardware.rfm9x.manager import RFM9xManager +from pysquared.hardware.rfm9x.modulation import RFM9xModulation +from pysquared.logger import Logger +from pysquared.pysquared import Satellite try: from typing import Any, Union diff --git a/lib/pysquared/config/config.py b/lib/pysquared/config/config.py deleted file mode 100644 index ced29a9..0000000 --- a/lib/pysquared/config/config.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Class for encapsulating config.json. The goal is to -distribute these values across the files & variables -that use them. Instantiation happens in main. - -Also it allow values to be set temporarily or permanently using the -Attempting to follow the FPrime model. -""" - -import json - -from lib.pysquared.config.radio import RadioConfig - - -class Config: - def __init__(self, config_path: str) -> None: - # parses json & assigns data to variables - with open(config_path, "r") as f: - json_data = json.loads(f.read()) - - self.radio: RadioConfig = RadioConfig(json_data["radio"]) - self.cubesat_name: str = json_data["cubesat_name"] - self.callsign: str = json_data["callsign"] - self.last_battery_temp: float = json_data["last_battery_temp"] - self.sleep_duration: int = json_data["sleep_duration"] - self.detumble_enable_z: bool = json_data["detumble_enable_z"] - self.detumble_enable_x: bool = json_data["detumble_enable_x"] - self.detumble_enable_y: bool = json_data["detumble_enable_y"] - self.jokes: list[str] = json_data["jokes"] - self.debug: bool = json_data["debug"] - self.legacy: bool = json_data["legacy"] - self.heating: bool = json_data["heating"] - self.orpheus: bool = json_data["orpheus"] - self.is_licensed: bool = json_data["is_licensed"] - self.normal_temp: int = json_data["normal_temp"] - self.normal_battery_temp: int = json_data["normal_battery_temp"] - self.normal_micro_temp: int = json_data["normal_micro_temp"] - self.normal_charge_current: float = json_data["normal_charge_current"] - self.normal_battery_voltage: float = json_data["normal_battery_voltage"] - self.critical_battery_voltage: float = json_data["critical_battery_voltage"] - self.battery_voltage: float = json_data["battery_voltage"] - self.current_draw: float = json_data["current_draw"] - self.reboot_time: int = json_data["reboot_time"] - self.turbo_clock: bool = json_data["turbo_clock"] - self.super_secret_code: str = json_data["super_secret_code"] - self.repeat_code: str = json_data["repeat_code"] - self.joke_reply: list[str] = json_data["joke_reply"] diff --git a/lib/pysquared/config/radio.py b/lib/pysquared/config/radio.py deleted file mode 100644 index e71ec93..0000000 --- a/lib/pysquared/config/radio.py +++ /dev/null @@ -1,25 +0,0 @@ -class RadioConfig: - def __init__(self, radio_dict: dict) -> None: - self.sender_id: int = radio_dict["sender_id"] - self.receiver_id: int = radio_dict["receiver_id"] - self.transmit_frequency: float = radio_dict["transmit_frequency"] - self.start_time: int = radio_dict["start_time"] - self.fsk: FSKConfig = FSKConfig(radio_dict["fsk"]) - self.lora: LORAConfig = LORAConfig(radio_dict["lora"]) - - -class FSKConfig: - def __init__(self, fsk_dict: dict) -> None: - self.broadcast_address: int = fsk_dict["broadcast_address"] - self.node_address: int = fsk_dict["node_address"] - self.modulation_type: int = fsk_dict["modulation_type"] - - -class LORAConfig: - def __init__(self, lora_dict: dict) -> None: - self.ack_delay: float = lora_dict["ack_delay"] - self.coding_rate: int = lora_dict["coding_rate"] - self.cyclic_redundancy_check: bool = lora_dict["cyclic_redundancy_check"] - self.max_output: bool = lora_dict["max_output"] - self.spreading_factor: int = lora_dict["spreading_factor"] - self.transmit_power: int = lora_dict["transmit_power"] diff --git a/lib/pysquared/functions.py b/lib/pysquared/functions.py old mode 100755 new mode 100644 index 862d31d..62379aa --- a/lib/pysquared/functions.py +++ b/lib/pysquared/functions.py @@ -9,13 +9,13 @@ import random import time -from lib.pysquared.config.config import Config -from lib.pysquared.hardware.rfm9x.manager import RFM9xManager -from lib.pysquared.logger import Logger -from lib.pysquared.packet_manager import PacketManager -from lib.pysquared.packet_sender import PacketSender -from lib.pysquared.pysquared import Satellite -from lib.pysquared.sleep_helper import SleepHelper +from pysquared.config.config import Config +from pysquared.hardware.rfm9x.manager import RFM9xManager +from pysquared.logger import Logger +from pysquared.packet_manager import PacketManager +from pysquared.packet_sender import PacketSender +from pysquared.pysquared import Satellite +from pysquared.sleep_helper import SleepHelper try: from typing import List, OrderedDict, Union @@ -188,7 +188,7 @@ def send_face(self) -> None: def listen(self) -> bool: # need to instanciate cdh to feed it the config var # assigned from the Config object - from lib.pysquared.cdh import CommandDataHandler + from pysquared.cdh import CommandDataHandler cdh = CommandDataHandler(self.config, self.logger, self.radio_manager) @@ -240,7 +240,7 @@ def all_face_data(self) -> list: ) try: - import lib.pysquared.Big_Data as Big_Data + import pysquared.Big_Data as Big_Data self.logger.debug( "Free Memory Stat after importing Big_data library", @@ -302,7 +302,7 @@ def detumble(self, dur: int = 7) -> None: self.cubesat.rgb = (255, 255, 255) try: - import lib.pysquared.Big_Data as Big_Data + import pysquared.Big_Data as Big_Data a: Big_Data.AllFaces = Big_Data.AllFaces(self.cubesat.tca, self.logger) except Exception as e: @@ -327,7 +327,7 @@ def actuate(dipole: list[float], duration) -> None: def do_detumble() -> None: try: - import lib.pysquared.detumble as detumble + import pysquared.detumble as detumble for _ in range(3): data = [self.cubesat.IMU.Gyroscope, self.cubesat.IMU.Magnetometer] diff --git a/lib/pysquared/hardware/__init__.py b/lib/pysquared/hardware/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/pysquared/hardware/busio.py b/lib/pysquared/hardware/busio.py deleted file mode 100644 index 63bbd34..0000000 --- a/lib/pysquared/hardware/busio.py +++ /dev/null @@ -1,49 +0,0 @@ -from busio import SPI -from microcontroller import Pin - -from lib.pysquared.hardware.decorators import with_retries -from lib.pysquared.hardware.exception import HardwareInitializationError -from lib.pysquared.logger import Logger - -try: - from typing import Optional -except ImportError: - pass - - -@with_retries(max_attempts=3, initial_delay=1) -def initialize_spi_bus( - logger: Logger, - clock: Pin, - mosi: Optional[Pin] = None, - miso: Optional[Pin] = None, - baudrate: Optional[int] = 100000, - phase: Optional[int] = 0, - polarity: Optional[int] = 0, - bits: Optional[int] = 8, -) -> SPI: - """Initializes a SPI bus" - - :param Logger logger: The logger instance to log messages. - :param Pin clock: The pin to use for the clock signal. - :param Pin mosi: The pin to use for the MOSI signal. - :param Pin miso: The pin to use for the MISO signal. - :param int baudrate: The baudrate of the SPI bus (default is 100000). - :param int phase: The phase of the SPI bus (default is 0). - :param int polarity: The polarity of the SPI bus (default is 0). - :param int bits: The number of bits per transfer (default is 8). - - :raises HardwareInitializationError: If the SPI bus fails to initialize. - - :return ~busio.SPI: The initialized SPI object. - """ - logger.debug("Initializing spi") - - try: - spi = SPI(clock, mosi, miso) - spi.try_lock() - spi.configure(baudrate, phase, polarity, bits) - spi.unlock() - return spi - except Exception as e: - raise HardwareInitializationError("Failed to initialize spi") from e diff --git a/lib/pysquared/hardware/decorators.py b/lib/pysquared/hardware/decorators.py deleted file mode 100644 index 90e0cbd..0000000 --- a/lib/pysquared/hardware/decorators.py +++ /dev/null @@ -1,36 +0,0 @@ -import time - -from lib.pysquared.hardware.exception import HardwareInitializationError - - -def with_retries(max_attempts: int = 3, initial_delay: float = 1.0): - """Decorator that retries hardware initialization with exponential backoff. - - :param int max_attempts: Maximum number of attempts to try initialization (default is 3) - :param int initial_delay: Initial delay in seconds between attempts (default is 1.0) - - :raises HardwareInitializationError: If all attempts fail, the last exception is raised - - :returns: The result of the decorated function if successful - """ - - def decorator(func): - def wrapper(*args, **kwargs): - last_exception = None - delay = initial_delay - - for attempt in range(max_attempts): - try: - return func(*args, **kwargs) - except HardwareInitializationError as e: - last_exception = e - if attempt < max_attempts - 1: # Don't sleep on last attempt - time.sleep(delay) - delay *= 2 # Exponential backoff - - # If we get here, all attempts failed - raise last_exception - - return wrapper - - return decorator diff --git a/lib/pysquared/hardware/digitalio.py b/lib/pysquared/hardware/digitalio.py deleted file mode 100644 index eda9919..0000000 --- a/lib/pysquared/hardware/digitalio.py +++ /dev/null @@ -1,35 +0,0 @@ -from digitalio import DigitalInOut, Direction -from microcontroller import Pin - -from lib.pysquared.hardware.decorators import with_retries -from lib.pysquared.hardware.exception import HardwareInitializationError -from lib.pysquared.logger import Logger - - -@with_retries(max_attempts=3, initial_delay=1) -def initialize_pin( - logger: Logger, pin: Pin, direction: Direction, initial_value: bool -) -> DigitalInOut: - """Initializes a DigitalInOut pin. - - :param Logger logger: The logger instance to log messages. - :param Pin pin: The pin to initialize. - :param Direction direction: The direction of the pin. - :param bool initial_value: The initial value of the pin (default is True). - - :raises HardwareInitializationError: If the pin fails to initialize. - - :return ~digitalio.DigitalInOut: The initialized DigitalInOut object. - """ - logger.debug( - message="Initializing pin", - initial_value=initial_value, - ) - - try: - digital_in_out = DigitalInOut(pin) - digital_in_out.direction = direction - digital_in_out.value = initial_value - return digital_in_out - except Exception as e: - raise HardwareInitializationError("Failed to initialize pin") from e diff --git a/lib/pysquared/hardware/exception.py b/lib/pysquared/hardware/exception.py deleted file mode 100644 index a4c2b65..0000000 --- a/lib/pysquared/hardware/exception.py +++ /dev/null @@ -1,2 +0,0 @@ -class HardwareInitializationError(Exception): - pass diff --git a/lib/pysquared/hardware/rfm9x/__init__.py b/lib/pysquared/hardware/rfm9x/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/pysquared/hardware/rfm9x/factory.py b/lib/pysquared/hardware/rfm9x/factory.py deleted file mode 100644 index a507c8f..0000000 --- a/lib/pysquared/hardware/rfm9x/factory.py +++ /dev/null @@ -1,172 +0,0 @@ -from lib.pysquared.config.radio import FSKConfig, LORAConfig, RadioConfig -from lib.pysquared.hardware.decorators import with_retries -from lib.pysquared.hardware.exception import HardwareInitializationError -from lib.pysquared.hardware.rfm9x.modulation import RFM9xModulation -from lib.pysquared.logger import Logger - -try: - from lib.adafruit_rfm.rfm9x import RFM9x - from lib.adafruit_rfm.rfm9xfsk import RFM9xFSK -except ImportError: - from mocks.circuitpython.adafruit_rfm.rfm9x import RFM9x # type: ignore - from mocks.circuitpython.adafruit_rfm.rfm9xfsk import RFM9xFSK # type: ignore - -# Type hinting only -try: - from busio import SPI - from digitalio import DigitalInOut - - from lib.adafruit_rfm.rfm_common import RFMSPI -except ImportError: - pass - - -class RFM9xFactory: - """Factory class for creating RFM9x radio instances. - The purpose of the factory class is to hide the complexity of radio initialization from the caller. - Specifically we should try to keep adafruit_rfm to only this factory class with the exception of the RFMSPI class. - """ - - def __init__( - self, - spi: SPI, - chip_select: DigitalInOut, - reset: DigitalInOut, - radio_config: RadioConfig, - ) -> None: - """Initialize the factory class. - - :param busio.SPI spi: The SPI bus connected to the chip. Ensure SCK, MOSI, and MISO are connected. - :param ~digitalio.DigitalInOut cs: A DigitalInOut object connected to the chip's CS/chip select line. - :param ~digitalio.DigitalInOut reset: A DigitalInOut object connected to the chip's RST/reset line. - :param RadioConfig radio_config: Radio config object. - """ - self._spi = spi - self._chip_select = chip_select - self._reset = reset - self._radio_config = radio_config - - @with_retries(max_attempts=3, initial_delay=1) - def create( - self, - logger: Logger, - modulation: RFM9xModulation, - ) -> RFMSPI: - """Create a RFM9x radio instance. - - :param Logger logger: Logger instance for logging messages. - :param RFM9xModulation modulation: Either FSK or LoRa. - - :raises HardwareInitializationError: If the radio fails to initialize. - - :return An instance of the RFMSPI class, either RFM9xFSK or RFM9x based on the mode. - """ - logger.debug(message="Initializing radio", modulation=modulation) - - try: - if modulation == RFM9xModulation.FSK: - radio: RFMSPI = self.create_fsk_radio( - self._spi, - self._chip_select, - self._reset, - self._radio_config.transmit_frequency, - self._radio_config.fsk, - ) - else: - radio: RFMSPI = self.create_lora_radio( - self._spi, - self._chip_select, - self._reset, - self._radio_config.transmit_frequency, - self._radio_config.lora, - ) - - radio.node = self._radio_config.sender_id - radio.destination = self._radio_config.receiver_id - - return radio - except Exception as e: - raise HardwareInitializationError( - "Failed to initialize radio with modulation {modulation}" - ) from e - - @staticmethod - def create_fsk_radio( - spi: SPI, - cs: DigitalInOut, - rst: DigitalInOut, - transmit_frequency: int, - fsk_config: FSKConfig, - ) -> RFMSPI: - """Create a FSK radio instance. - - :param busio.SPI spi: The SPI bus connected to the chip. Ensure SCK, MOSI, and MISO are connected. - :param ~digitalio.DigitalInOut cs: A DigitalInOut object connected to the chip's CS/chip select line. - :param ~digitalio.DigitalInOut reset: A DigitalInOut object connected to the chip's RST/reset line. - :param int transmit_frequency: Frequency at which the radio will transmit. - :param FSKConfig config: FSK config object. - - :return An instance of :class:`~adafruit_rfm.rfm9xfsk.RFM9xFSK`. - """ - radio: RFM9xFSK = RFM9xFSK( - spi, - cs, - rst, - transmit_frequency, - ) - - radio.fsk_broadcast_address = fsk_config.broadcast_address - radio.fsk_node_address = fsk_config.node_address - radio.modulation_type = fsk_config.modulation_type - - return radio - - @staticmethod - def create_lora_radio( - spi: SPI, - cs: DigitalInOut, - rst: DigitalInOut, - transmit_frequency: int, - lora_config: LORAConfig, - ) -> RFMSPI: - """Create a LoRa radio instance. - - :param busio.SPI spi: The SPI bus connected to the chip. Ensure SCK, MOSI, and MISO are connected. - :param ~digitalio.DigitalInOut cs: A DigitalInOut object connected to the chip's CS/chip select line. - :param ~digitalio.DigitalInOut reset: A DigitalInOut object connected to the chip's RST/reset line. - :param int transmit_frequency: Frequency at which the radio will transmit. - :param LORAConfig config: LoRa config object. - - :return An instance of the RFM9x class. - """ - radio: RFM9x = RFM9x( - spi, - cs, - rst, - transmit_frequency, - ) - - radio.ack_delay = lora_config.ack_delay - radio.enable_crc = lora_config.cyclic_redundancy_check - radio.max_output = lora_config.max_output - radio.spreading_factor = lora_config.spreading_factor - radio.tx_power = lora_config.transmit_power - - if radio.spreading_factor > 9: - radio.preamble_length = radio.spreading_factor - radio.low_datarate_optimize = 1 - - return radio - - @staticmethod - def get_instance_modulation(radio: RFMSPI) -> RFM9xModulation: - """Determine the radio modulation in use. - - :param RFMSPI radio: The radio instance to check. - - :return The modulation in use. - """ - if isinstance(radio, RFM9xFSK): - return RFM9xModulation.FSK - - return RFM9xModulation.LORA diff --git a/lib/pysquared/hardware/rfm9x/manager.py b/lib/pysquared/hardware/rfm9x/manager.py deleted file mode 100644 index 4790213..0000000 --- a/lib/pysquared/hardware/rfm9x/manager.py +++ /dev/null @@ -1,105 +0,0 @@ -from lib.pysquared.hardware.rfm9x.modulation import RFM9xModulation - -# Type hinting only -try: - from typing import Any - - from lib.adafruit_rfm.rfm_common import RFMSPI - from lib.pysquared.hardware.rfm9x.factory import RFM9xFactory - from lib.pysquared.logger import Logger - from lib.pysquared.nvm.flag import Flag -except ImportError: - pass - - -class RFM9xManager: - """Manages the lifecycle and mode switching of the RFM9x radio.""" - - _radio: RFMSPI | None = None - - def __init__( - self, - logger: Logger, - use_fsk: Flag, - radio_factory: RFM9xFactory, - is_licensed: bool, - ) -> None: - """Initialize the rfm9x manager. - - Stores configuration but doesn't create radio until needed. - - :param Logger logger: Logger instance for logging messages. - :param Flag use_fsk: Flag to determine whether to use FSK or LoRa mode. - :param RFM9xFactory radio_factory: Factory for creating RFM9x radio instances. - - :raises HardwareInitializationError: If the radio fails to initialize. - """ - self._log = logger - self._use_fsk = use_fsk - self._radio_factory = radio_factory - self._is_licensed = is_licensed - - self._radio = self.radio - - @property - def radio(self) -> RFMSPI: - """Get the current radio instance, creating it if needed. - :return ~lib.adafruit_rfm.rfm_common.RFMSPI: The RFM9x radio instance. - """ - if self._radio is None: - self._radio = self._radio_factory.create( - self._log, - self.get_modulation(), - ) - - # Always toggle back to LoRa on reboot - self.set_modulation(RFM9xModulation.LORA) - - return self._radio - - def beacon_radio_message(self, msg: Any) -> None: - """Beacon a radio message and log the result.""" - try: - if not self._is_licensed: - raise ValueError("Radio is not licensed") - - sent = self.radio.send(bytes(msg, "UTF-8")) - except Exception as e: - self._log.error("There was an error while beaconing", e) - return - - self._log.info("I am beaconing", beacon=str(msg), success=str(sent)) - - def get_modulation(self) -> str: - """Get the current radio modulation. - :return str: The current radio modulation. - """ - if self._radio is None: - return RFM9xModulation.FSK if self._use_fsk.get() else RFM9xModulation.LORA - - return self._radio_factory.get_instance_modulation(self.radio) - - def set_modulation(self, req_modulation: RFM9xModulation) -> None: - """ - Set the radio modulation. - Takes effect on the next reboot. - :param lib.radio.RFM9xModulation req_modulation: The modulation to switch to. - :return: None - """ - if self.get_modulation() != req_modulation: - self._use_fsk.toggle(req_modulation == RFM9xModulation.FSK) - self._log.info( - "Radio modulation change requested", modulation=req_modulation - ) - - def get_temperature(self) -> int: - """Get the temperature from the radio. - :return int: The temperature in degrees Celsius. - """ - raw_temp = self.radio.read_u8(0x5B) - temp = raw_temp & 0x7F - if (raw_temp & 0x80) == 0x80: - temp = ~temp + 0x01 - - prescaler = 143 - return temp + prescaler diff --git a/lib/pysquared/hardware/rfm9x/modulation.py b/lib/pysquared/hardware/rfm9x/modulation.py deleted file mode 100644 index fe41ae8..0000000 --- a/lib/pysquared/hardware/rfm9x/modulation.py +++ /dev/null @@ -1,5 +0,0 @@ -class RFM9xModulation: - """Enumeration for the RFM9x radio modulation options.""" - - FSK = "FSK" # Frequency-shift Keying - LORA = "LoRa" # Long Range diff --git a/lib/pysquared/logger.py b/lib/pysquared/logger.py index 1dcfd5f..af01f70 100644 --- a/lib/pysquared/logger.py +++ b/lib/pysquared/logger.py @@ -8,7 +8,7 @@ import traceback from collections import OrderedDict -from lib.pysquared.nvm.counter import Counter +from pysquared.nvm.counter import Counter def _color(msg, color="gray", fmt="normal"): diff --git a/lib/pysquared/nvm/__init__.py b/lib/pysquared/nvm/__init__.py deleted file mode 100644 index 762cffd..0000000 --- a/lib/pysquared/nvm/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -The NVM package is a collection of functionality that interacts with non-volatile memory -""" diff --git a/lib/pysquared/nvm/counter.py b/lib/pysquared/nvm/counter.py deleted file mode 100644 index 8af7d35..0000000 --- a/lib/pysquared/nvm/counter.py +++ /dev/null @@ -1,27 +0,0 @@ -try: - from stubs.circuitpython.byte_array import ByteArray -except ImportError: - pass - - -class Counter: - def __init__( - self, - index: int, - datastore: ByteArray, - ) -> None: - self._index = index - self._datastore = datastore - - def get(self) -> int: - """ - get returns the value of the counter - """ - return self._datastore[self._index] - - def increment(self) -> None: - """ - increment increases the counter by one - """ - value: int = (self.get() + 1) & 0xFF # 8-bit counter with rollover - self._datastore[self._index] = value diff --git a/lib/pysquared/nvm/flag.py b/lib/pysquared/nvm/flag.py deleted file mode 100755 index 0ef062a..0000000 --- a/lib/pysquared/nvm/flag.py +++ /dev/null @@ -1,30 +0,0 @@ -try: - from stubs.circuitpython.byte_array import ByteArray -except ImportError: - pass - - -class Flag: - """ - Flag class for managing boolean flags stored in non-volatile memory - """ - - def __init__(self, index: int, bit_index: int, datastore: ByteArray) -> None: - self._index = index # Index of specific byte in array of bytes - self._bit = bit_index # Position of bit within specific byte - self._datastore = datastore # Array of bytes (Non-volatile Memory) - self._bit_mask = 1 << bit_index # Creating bitmask with bit position - # Ex. bit = 3 -> 3 % 8 = 3 -> 1 << 3 = 00001000 - - def get(self) -> bool: - """Get flag value""" - return bool(self._datastore[self._index] & self._bit_mask) - - def toggle(self, value: bool) -> None: - """Toggle flag value""" - if value: - # If true, perform OR on specific byte and bitmask to set bit to 1 - self._datastore[self._index] |= self._bit_mask - else: - # If false, perform AND on specific byte and inverted bitmask to set bit to 0 - self._datastore[self._index] &= ~self._bit_mask diff --git a/lib/pysquared/nvm/register.py b/lib/pysquared/nvm/register.py deleted file mode 100644 index 885fa9d..0000000 --- a/lib/pysquared/nvm/register.py +++ /dev/null @@ -1,6 +0,0 @@ -from micropython import const - -# NVM register numbers -BOOTCNT = const(0) -ERRORCNT = const(7) -FLAG = const(16) diff --git a/lib/pysquared/packet_manager.py b/lib/pysquared/packet_manager.py index a2493c1..4e61a12 100644 --- a/lib/pysquared/packet_manager.py +++ b/lib/pysquared/packet_manager.py @@ -1,6 +1,6 @@ # Written with Claude 3.5 # Nov 10, 2024 -from lib.pysquared.logger import Logger +from pysquared.logger import Logger try: from typing import Union diff --git a/lib/pysquared/packet_sender.py b/lib/pysquared/packet_sender.py index 9020d27..1d564af 100644 --- a/lib/pysquared/packet_sender.py +++ b/lib/pysquared/packet_sender.py @@ -1,6 +1,6 @@ -from lib.pysquared.hardware.rfm9x.manager import RFM9xManager -from lib.pysquared.logger import Logger -from lib.pysquared.packet_manager import PacketManager +from pysquared.hardware.rfm9x.manager import RFM9xManager +from pysquared.logger import Logger +from pysquared.packet_manager import PacketManager try: from typing import Union diff --git a/lib/pysquared/pysquared.py b/lib/pysquared/pysquared.py index 6f3ce93..5dda946 100644 --- a/lib/pysquared/pysquared.py +++ b/lib/pysquared/pysquared.py @@ -24,12 +24,12 @@ import lib.adafruit_lis2mdl as adafruit_lis2mdl # Magnetometer import lib.adafruit_tca9548a as adafruit_tca9548a # I2C Multiplexer import lib.neopixel as neopixel # RGB LED -import lib.pysquared.nvm.register as register import lib.rv3028.rv3028 as rv3028 # Real Time Clock +import pysquared.nvm.register as register from lib.adafruit_lsm6ds.lsm6dsox import LSM6DSOX # IMU -from lib.pysquared.config.config import Config # Configs -from lib.pysquared.nvm.counter import Counter -from lib.pysquared.nvm.flag import Flag +from pysquared.config.config import Config # Configs +from pysquared.nvm.counter import Counter +from pysquared.nvm.flag import Flag try: from typing import Any, Callable, Optional, OrderedDict, TextIO, Union @@ -38,7 +38,7 @@ except Exception: pass -from lib.pysquared.logger import Logger +from pysquared.logger import Logger SEND_BUFF: bytearray = bytearray(252) diff --git a/lib/pysquared/rtc/__init__.py b/lib/pysquared/rtc/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/pysquared/rtc/rp2040.py b/lib/pysquared/rtc/rp2040.py deleted file mode 100644 index 9b9ecf1..0000000 --- a/lib/pysquared/rtc/rp2040.py +++ /dev/null @@ -1,38 +0,0 @@ -import time - -try: - import rtc -except ImportError: - import mocks.circuitpython.rtc as rtc - - -class RP2040RTC: - """ - Class for interfacing with the RP2040's Real Time Clock (RTC) - """ - - @staticmethod - def set_time( - year: int, - month: int, - date: int, - hour: int, - minute: int, - second: int, - day_of_week: int, - ) -> None: - """ - Updates the RP2040's Real Time Clock (RTC) to the date and time passed - - :param year: The year value (0-9999) - :param month: The month value (1-12) - :param date: The date value (1-31) - :param hour: The hour value (0-23) - :param minute: The minute value (0-59) - :param second: The second value (0-59) - :param day_of_week: The nth day of the week (0-6), where 0 represents Sunday and 6 represents Saturday - """ - rp2040_rtc = rtc.RTC() - rp2040_rtc.datetime = time.struct_time( - (year, month, date, hour, minute, second, day_of_week, -1, -1) - ) diff --git a/lib/pysquared/rtc/rtc_common.py b/lib/pysquared/rtc/rtc_common.py deleted file mode 100644 index 9b2d806..0000000 --- a/lib/pysquared/rtc/rtc_common.py +++ /dev/null @@ -1,22 +0,0 @@ -import time - -try: - import rtc -except ImportError: - import mocks.circuitpython.rtc as rtc - - -class RTC: - """ - Common class for interfacing with the Real Time Clock (RTC) - """ - - @staticmethod - def init() -> None: - """ - Initialize the RTC - - Required on every boot to ensure the RTC is ready for use - """ - rp2040_rtc = rtc.RTC() - rp2040_rtc.datetime = time.localtime() diff --git a/lib/pysquared/sleep_helper.py b/lib/pysquared/sleep_helper.py index 070c637..5cf7b35 100644 --- a/lib/pysquared/sleep_helper.py +++ b/lib/pysquared/sleep_helper.py @@ -4,8 +4,8 @@ import alarm import digitalio -from lib.pysquared.logger import Logger -from lib.pysquared.pysquared import Satellite +from pysquared.logger import Logger +from pysquared.pysquared import Satellite try: from typing import Literal diff --git a/lib/pysquared/usb/usbfunctions.py b/lib/pysquared/usb/usbfunctions.py deleted file mode 100644 index 1b57756..0000000 --- a/lib/pysquared/usb/usbfunctions.py +++ /dev/null @@ -1,348 +0,0 @@ -import os -import sys -import traceback -from typing import Union - -import board -import busio -import microcontroller -import sdcardio -import storage - -# Helpful resource: https://docs.circuitpython.org/en/latest/shared-bindings/storage/ - -# boot.py: -# import storage -# import json -# with open("parameters.json", "r") as f: -# json_data = f.read() -# parameters = json.loads(json_data) -# if parameters["read_state"]: -# storage.enable_usb_drive() -# else: -# storage.disable_usb_drive() - -# Next step: Variable needed in config file to determine USB on or off state -# (call storage.enable_usb_drive() in boot.py based on config value) - - -class USBFunctions: - """Class providing various functionalities related to USB and SD card operations.""" - - """Initializing class, remounting storage, and initializing SD card""" - - def __init__(self) -> None: - storage.remount("/", readonly=False) # Remounts root file system as readable - self.sd_initialized = False # Creating SD initialization flag - if self.init_sd(): # Checking if SD initialization via init_sd() was successful - self.sd_initialized = True # Setting flag to True upon success - print("Initialized USB Functionalities") - - def disable_write(self) -> None: - """Disables write access by inserting a line in a JSON file.""" - self.insert_data("/parameters.json", 2, '"read_state": false\n') - # Note: This can be changed in the context of our config.json file - - def enable_write(self, reset: bool = False) -> None: - """Enables write access by inserting a line in a JSON file. - - Args: - reset (bool): If True, resets the microcontroller. - """ - self.insert_data("/parameters.json", 2, '"read_state": true\n') - # Note: This can be changed in the context of our config.json file - if reset: - microcontroller.reset() - - def insert_data(self, filename: str, line: int, data: str) -> None: - """Inserts data into a specific line of a file. - - Args: - filename (str): The name of the file. - line (int): The line number to insert data. - data (str): The data to insert. - """ - with open(filename, "r") as f: # Opens file in read mode - lines = f.readlines() # Read all lines from file - - lines[line - 1] = data # Replaces specified line with new data - - with open(filename, "w") as f: # Opens file in write mode - f.write("".join(map(str, lines))) # Writes modified lines back to the file - - def init_sd(self) -> bool: - """Initializes the SD card. - - Returns: - bool: True if initialization is successful, False otherwise. - """ - spi_bus = busio.SPI( - board.SPI1_SCK, board.SPI1_MOSI, board.SPI1_MISO - ) # Sets up the SPI bus - _sd = sdcardio.SDCard( - spi_bus, board.SPI1_CS0, baudrate=4000000 - ) # Initializes the SD card with the SPI bus - _vfs = storage.VfsFat(_sd) # Creates a FAT filesystem on the SD card - storage.mount(_vfs, "/sd") # Mounts the filesystem to the /sd directory - self.fs = _vfs # Stores the filesystem object - sys.path.append("/sd") # Adds the /sd directory to the system path - return True # Returns True indicating successful initialization - - def set_sd_file_system(self) -> None: - """Creates a file on the SD card.""" - self.make_file("/data/temperature.txt") - - def make_file(self, file_name: str, binary: bool = False) -> Union[str, None]: - """Creates a new file in the specified directory. - - Args: - file_name (str): The name of the file to create. - binary (bool): If True, creates the file in binary mode. - - Returns: - str: The name of the created file. - """ - try: - ff = "" - n = 0 - _folder = file_name[ - : file_name.rfind("/") + 1 - ] # Extracts the folder from the file name - _file = file_name[ - file_name.rfind("/") + 1 : file_name.rfind(".") - ] # Extracts the file prefix from the file name - _file_type = file_name[ - file_name.rfind(".") + 1 : - ] # Extracts the file type from the file name - print( - "Creating new file in directory: /sd{} with file prefix: {}".format( - _folder, _file - ) - ) - try: - os.chdir( - "/sd" + _folder - ) # Changes the current directory to the specified folder - except OSError: - print("Directory {} not found. Creating...".format(_folder)) - try: - os.mkdir("/sd" + _folder) # Creates the folder if it doesn't exist - except Exception as e: - print( - "Error with creating new file: " - + "".join(traceback.format_exception(e)) - ) - return None - for i in range( - 0xFFFF - ): # Finding an available, unique file name by appending a unique number - ff = "/sd{}{}{:05}.{}".format( - _folder, _file, (n + i) % 0xFFFF, _file_type - ) - try: - if n is not None: - os.stat(ff) # Checks if the file exists - except Exception: - n = (n + i) % 0xFFFF - # print('file number is',n) - break - print("creating file..." + str(ff)) - if binary: - b = "ab" # Sets mode to binary append - else: - b = "a" # Sets mode to append - with open(ff, b) as f: - f.tell() # Returns current file position - os.chdir("/") # Changes back to the root directory - return ff - except Exception as e: - print("Error creating file: " + "".join(traceback.format_exception(e))) - return None - - def readfile(self, path: str, type: str = "string") -> Union[str, list]: - """Reads a file and returns its contents. - - Args: - path (str): The path to the file. - type (str): The type of return value, either 'string' or 'list'. - - Returns: - str: The contents of the file as a string or list. - """ - with open(path, "r") as f: # Opens the file at the specified path in read mode - lines = ( - f.readlines() - ) # Reads all lines from the file and stores them in a list - if type == "list": # Checks if the return type is specified as 'list' - return lines # Returns the list of lines - if type == "string": # Checks if the return type is specified as 'string' - return "".join( - map(str, lines) - ) # Joins the list of lines into a single string and returns it - - def writefile(self, path: str, contents: str) -> str: - """Writes contents to a file and returns the updated file contents. - - Args: - path (str): The path to the file. - contents (str): The contents to write to the file. - - Returns: - str: The updated file contents. - """ - with open(path, "w") as f: # Opens the file at the specified path in write mode - f.write(contents) # Writes the provided contents to the file - return self.readfile(path) # Reads and returns the updated file contents - - def appendfile(self, path: str, contents: str) -> str: - """Appends contents to a file and returns the updated file contents. - - Args: - path (str): The path to the file. - contents (str): The contents to append to the file. - - Returns: - str: The updated file contents. - """ - with open( - path, "a+" - ) as f: # Opens the file at the specified path in append mode - f.write(contents) # Appends the provided contents to the file - return self.readfile(path) # Reads and returns the updated file contents - - def replace_line_in_file(self, path: str, line: int, contents: str) -> str: - """Replaces a specific line in a file and returns the updated file contents. - - Args: - path (str): The path to the file. - line (int): The line number to replace. - contents (str): The new contents for the line. - - Returns: - str: The updated file contents. - """ - lines = self.readfile(path, "list") # Reads the file as a list of lines - if not isinstance(lines, list): # Type check to make sure lines is a list - raise TypeError( - "Expected lines to be a list, but got {}".format(type(lines)) - ) - - if line < 1 or line > len( - lines - ): # Checks if line number is in range within the file - raise IndexError("Line number {} is out of range".format(line)) - - lines[line - 1] = contents # Replaces the specified line with the new contents - _stringify = "".join( - map(str, lines) - ) # Joins the list of lines into a single string - - return self.writefile( - path, _stringify - ) # Writes the updated contents to the file and returns the updated file contents - - def insert_line_in_file(self, path: str, line: int, contents: str) -> str: - """Inserts a line at a specific position in a file and returns the updated file contents. - - Args: - path (str): The path to the file. - line (int): The line number to insert at. - contents (str): The contents to insert. - - Returns: - str: The updated file contents. - """ - lines = self.readfile(path, type="list") # Reads the file as a list of lines - if not isinstance(lines, list): # Type check to make sure lines is a list - raise TypeError( - "Expected lines to be a list, but got {}".format(type(lines)) - ) - - _intermediate = lines[ - line - 1 : - ] # Stores the lines from the specified position onward - lines[line:] = _intermediate # Shifts the lines to make space for the new line - lines[line - 1] = contents # Inserts the new line - _stringify = "".join( - map(str, lines) - ) # Joins the list of lines into a single string - - return self.writefile( - path, _stringify - ) # Writes the updated contents to the file and returns the updated file contents - - def copyfile(self, to_path: str, from_path: str) -> str: - """Copies contents from one file to another. - - Args: - to_path (str): The destination file path. - from_path (str): The source file path. - - Returns: - str: The updated contents of the destination file. - """ - # TODO: Make file if not exist. look into why from_path file get rewritten. bug? - contents = self.readfile(from_path) # Reads the contents of the source file - return self.writefile( - to_path, contents - ) # Writes the contents to the destination file and returns the updated contents - - def print_directory(self, path: str, tabs: int = 0) -> None: - """Prints the contents of a directory recursively. - - Args: - path (str): The path to the directory. - tabs (int): The number of tabs for indentation. - """ - for file in os.listdir(path): # Lists all files in the directory - if file == "?": # Skips files named "?" - continue # Issue noted in Learn - stats = os.stat(path + "/" + file) # Gets the file statistics - filesize = stats[6] # Gets the file size - isdir = stats[0] & 0x4000 # Checks if the file is a directory - - if filesize < 1000: - sizestr = str(filesize) + " by" # Formats the file size in bytes - elif filesize < 1000000: - sizestr = "%0.1f KB" % ( - filesize / 1000 - ) # Formats the file size in kilobytes - else: - sizestr = "%0.1f MB" % ( - filesize / 1000000 - ) # Formats the file size in megabytes - - prettyprintname = "" - for _ in range(tabs): - prettyprintname += " " # Adds indentation based on the number of tabs - prettyprintname += file - if isdir: - prettyprintname += "/" # Adds a slash to indicate a directory - print("{0:<40} Size: {1:>10}".format(prettyprintname, sizestr)) - - # Recursively print directory contents - if isdir: - self.print_directory(path + "/" + file, tabs + 1) - - def delete_file(self, path: str) -> None: - """Deletes a specified file. - - Args: - path (str): The path to the file. - """ - os.remove(path) # Removes the file at the specified path - - def delete_directory(self, path: str, recursive: bool = False) -> None: - """Deletes a specified directory. - - Args: - path (str): The path to the directory. - recursive (bool): If True, deletes the directory recursively. - """ - # TODO: add recursive functionality to remove files - # Logic to check if directory is not empty. If it is delete regardless of recursion, if it is not check for if recursion is enabled - if path == "/sd": - print("you really shouldnt try to delete your root directory") - else: - os.chdir("/sd") # Changes the current directory to /sd - os.rmdir(path) # Removes the directory at the specified path diff --git a/lib/requirements.txt b/lib/requirements.txt index 2286cc2..d4b0d17 100644 --- a/lib/requirements.txt +++ b/lib/requirements.txt @@ -10,3 +10,4 @@ adafruit-circuitpython-rfm==1.0.3 adafruit-circuitpython-tca9548a @ git+https://github.com/proveskit/Adafruit_CircuitPython_TCA9548A adafruit-circuitpython-ticks==1.1.1 adafruit-circuitpython-veml7700==2.0.2 +pysquared @ git+https://github.com/hrfarmer/pysquared From 7f1b5843af6f82cdbe403179bc5c4e8e84656e31 Mon Sep 17 00:00:00 2001 From: aychar <58487401+hrfarmer@users.noreply.github.com> Date: Wed, 26 Mar 2025 01:46:58 -0500 Subject: [PATCH 4/8] pull from main repo --- .gitignore | 7 ++----- Makefile | 2 +- pyproject.toml | 1 + 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index e90dc23..053d20e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,5 @@ venv **/*.mpy # libs -lib/rv3028* -lib/adafruit* -lib/asyncio* -lib/neopixel.py -lib/pysquared/ +/lib/* +!/lib/requirements.txt diff --git a/Makefile b/Makefile index 43a81ff..070a1f5 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ download-libraries: .venv ## Download the required libraries @if [ -n "$(LOCAL_PYSQUARED)" ]; then \ $(UV) pip install $(LOCAL_PYSQUARED) --target lib --no-deps --upgrade --quiet; \ else \ - $(UV) pip install git+https://github.com/hrfarmer/pysquared --target lib --no-deps --upgrade --quiet; \ + $(UV) pip install git+https://github.com/proveskit/pysquared --target lib --no-deps --upgrade --quiet; \ fi @rm -rf lib/*.dist-info diff --git a/pyproject.toml b/pyproject.toml index bcb62d0..0402196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ omit = [ "lib/asyncio_*/**", "lib/rv3028*/**", "lib/neopixel.py", + "lib/pysquared/**" ] [tool.coverage.html] From 8b27cb5ee097426010533564282fd430082118a9 Mon Sep 17 00:00:00 2001 From: aychar <58487401+hrfarmer@users.noreply.github.com> Date: Wed, 26 Mar 2025 01:53:46 -0500 Subject: [PATCH 5/8] delete pysquared --- lib/pysquared/Big_Data.py | 115 ----- lib/pysquared/__init__.py | 0 lib/pysquared/cdh.py | 195 --------- lib/pysquared/detumble.py | 23 - lib/pysquared/functions.py | 352 ---------------- lib/pysquared/logger.py | 141 ------- lib/pysquared/packet_manager.py | 137 ------ lib/pysquared/packet_sender.py | 213 ---------- lib/pysquared/pysquared.py | 726 -------------------------------- lib/pysquared/sleep_helper.py | 107 ----- 10 files changed, 2009 deletions(-) delete mode 100644 lib/pysquared/Big_Data.py delete mode 100644 lib/pysquared/__init__.py delete mode 100644 lib/pysquared/cdh.py delete mode 100644 lib/pysquared/detumble.py delete mode 100644 lib/pysquared/functions.py delete mode 100644 lib/pysquared/logger.py delete mode 100644 lib/pysquared/packet_manager.py delete mode 100644 lib/pysquared/packet_sender.py delete mode 100644 lib/pysquared/pysquared.py delete mode 100644 lib/pysquared/sleep_helper.py diff --git a/lib/pysquared/Big_Data.py b/lib/pysquared/Big_Data.py deleted file mode 100644 index 0031b63..0000000 --- a/lib/pysquared/Big_Data.py +++ /dev/null @@ -1,115 +0,0 @@ -import gc - -import lib.adafruit_tca9548a as adafruit_tca9548a # I2C Multiplexer -from pysquared.logger import Logger - -try: - from typing import Union - -except Exception: - pass - - -class Face: - def __init__( - self, add: int, pos: str, tca: adafruit_tca9548a.TCA9548A, logger: Logger - ) -> None: - self.tca: adafruit_tca9548a.TCA9548A = tca - self.address: int = add - self.position: str = pos - self.logger: Logger = logger - - # Use tuple instead of list for immutable data - self.senlist: tuple = () - # Define sensors based on position using a dictionary lookup instead of if-elif chain - sensor_map: dict[str, tuple[str, ...]] = { - "x+": ("MCP", "VEML", "DRV"), - "x-": ("MCP", "VEML"), - "y+": ("MCP", "VEML", "DRV"), - "y-": ("MCP", "VEML"), - "z-": ("MCP", "VEML", "DRV"), - } - self.senlist: tuple[str, ...] = sensor_map.get(pos, ()) - - # Initialize sensor states dict only with needed sensors - self.sensors: dict[str, bool] = {sensor: False for sensor in self.senlist} - - # Initialize sensor objects as None - self.mcp = None - self.veml = None - self.drv = None - - def sensor_init(self, senlist, address) -> None: - gc.collect() # Force garbage collection before initializing sensors - - if "MCP" in senlist: - try: - import lib.adafruit_mcp9808 as adafruit_mcp9808 - - self.mcp: adafruit_mcp9808.MCP9808 = adafruit_mcp9808.MCP9808( - self.tca[address], address=27 - ) - self.sensors["MCP"] = True - except Exception as e: - self.logger.error("Error Initializing Temperature Sensor", e) - - if "VEML" in senlist: - try: - import lib.adafruit_veml7700 as adafruit_veml7700 - - self.veml: adafruit_veml7700.VEML7700 = adafruit_veml7700.VEML7700( - self.tca[address] - ) - self.sensors["VEML"] = True - except Exception as e: - self.logger.error("Error Initializing Light Sensor", e) - - if "DRV" in senlist: - try: - import lib.adafruit_drv2605 as adafruit_drv2605 - - self.drv: adafruit_drv2605.DRV2605 = adafruit_drv2605.DRV2605( - self.tca[address] - ) - self.sensors["DRV"] = True - except Exception as e: - self.logger.error("Error Initializing Motor Driver", e) - - gc.collect() # Clean up after initialization - - -class AllFaces: - def __init__(self, tca: adafruit_tca9548a.TCA9548A, logger: Logger) -> None: - self.tca: adafruit_tca9548a.TCA9548A = tca - self.faces: list[Face] = [] - self.logger: Logger = logger - - # Create faces using a loop instead of individual variables - positions: list[tuple[str, int]] = [ - ("y+", 0), - ("y-", 1), - ("x+", 2), - ("x-", 3), - ("z-", 4), - ] - for pos, addr in positions: - face: Face = Face(addr, pos, tca, self.logger) - face.sensor_init(face.senlist, face.address) - self.faces.append(face) - gc.collect() # Clean up after each face initialization - - def face_test_all(self) -> list[list[float]]: - results: list[list[float]] = [] - for face in self.faces: - if face: - try: - temp: Union[float, None] = ( - face.mcp.temperature if face.sensors.get("MCP") else None - ) - light: Union[float, None] = ( - face.veml.lux if face.sensors.get("VEML") else None - ) - results.append([temp, light]) - except Exception: - results.append([None, None]) - return results diff --git a/lib/pysquared/__init__.py b/lib/pysquared/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/pysquared/cdh.py b/lib/pysquared/cdh.py deleted file mode 100644 index 4173752..0000000 --- a/lib/pysquared/cdh.py +++ /dev/null @@ -1,195 +0,0 @@ -import random -import time - -from pysquared.config.config import Config -from pysquared.hardware.rfm9x.manager import RFM9xManager -from pysquared.hardware.rfm9x.modulation import RFM9xModulation -from pysquared.logger import Logger -from pysquared.pysquared import Satellite - -try: - from typing import Any, Union - - import circuitpython_typing -except Exception: - pass - - -class CommandDataHandler: - """ - Constructor - """ - - def __init__( - self, - config: Config, - logger: Logger, - radio_manager: RFM9xManager, - ) -> None: - self.logger: Logger = logger - self._commands: dict[bytes, str] = { - b"\x8eb": "noop", - b"\xd4\x9f": "hreset", - b"\x12\x06": "shutdown", - b"8\x93": "query", - b"\x96\xa2": "exec_cmd", - b"\xa5\xb4": "joke_reply", - b"\x56\xc4": "FSK", - } - self._joke_reply: list[str] = config.joke_reply - self._super_secret_code: bytes = config.super_secret_code.encode("utf-8") - self._repeat_code: bytes = config.repeat_code.encode("utf-8") - self.logger.info( - "The satellite has a super secret code!", - super_secret_code=self._super_secret_code, - ) - - self.radio_manager = radio_manager - - ############### hot start helper ############### - def hotstart_handler(self, cubesat: Satellite, msg: Any) -> None: - # check that message is for me - if msg[0] == self.radio_manager.radio.node: - # TODO check for optional radio config - - # manually send ACK - self.radio_manager.radio.send("!", identifier=msg[2], flags=0x80) - # TODO remove this delay. for testing only! - time.sleep(0.5) - self.message_handler(cubesat, msg) - else: - self.logger.info( - "Message not for me?", - target_id=hex(msg[0]), - my_id=hex(self.radio_manager.radio.node), - ) - - ############### message handler ############### - def message_handler(self, cubesat: Satellite, msg: bytearray) -> None: - multi_msg: bool = False - if len(msg) >= 10: # [RH header 4 bytes] [pass-code(4 bytes)] [cmd 2 bytes] - if bytes(msg[4:8]) == self._super_secret_code: - # check if multi-message flag is set - if msg[3] & 0x08: - multi_msg = True - # strip off RH header - msg: bytes = bytes(msg[4:]) - cmd: bytes = msg[4:6] # [pass-code(4 bytes)] [cmd 2 bytes] [args] - cmd_args: Union[bytes, None] = None - if len(msg) > 6: - self.logger.info("This is a command with args") - try: - cmd_args = msg[6:] # arguments are everything after - self.logger.info( - "Here are the command arguments", cmd_args=cmd_args - ) - except Exception as e: - self.logger.error("There was an error decoding the arguments", e) - if cmd in self._commands: - try: - if cmd_args is None: - self.logger.info( - "There are no args provided", command=self._commands[cmd] - ) - # eval a string turns it into a func name - eval(self._commands[cmd])(cubesat) - else: - self.logger.info( - "running command with args", - command=self._commands[cmd], - cmd_args=cmd_args, - ) - eval(self._commands[cmd])(cubesat, cmd_args) - except Exception as e: - self.logger.error("something went wrong!", e) - self.radio_manager.radio.send(str(e).encode()) - else: - self.logger.info("invalid command!") - self.radio_manager.radio.send(b"invalid cmd" + msg[4:]) - # check for multi-message mode - if multi_msg: - # TODO check for optional radio config - self.logger.info("multi-message mode enabled") - response = self.radio_manager.radio.receive( - keep_listening=True, - with_ack=True, - with_header=True, - view=True, - timeout=10, - ) - if response is not None: - cubesat.c_gs_resp += 1 - self.message_handler(cubesat, response) - elif bytes(msg[4:6]) == self._repeat_code: - self.logger.info("Repeating last message!") - try: - self.radio_manager.radio.send(msg[6:]) - except Exception as e: - self.logger.error("There was an error repeating the message!", e) - else: - self.logger.info("bad code?") - - ########### commands without arguments ########### - def noop(self) -> None: - self.logger.info("no-op") - - def hreset(self, cubesat: Satellite) -> None: - self.logger.info("Resetting") - try: - self.radio_manager.radio.send(data=b"resetting") - cubesat.micro.on_next_reset(cubesat.micro.RunMode.NORMAL) - cubesat.micro.reset() - except Exception: - pass - - def fsk(self) -> None: - self.radio_manager.set_modulation(RFM9xModulation.FSK) - - def joke_reply(self, cubesat: Satellite) -> None: - joke: str = random.choice(self._joke_reply) - self.logger.info("Sending joke reply", joke=joke) - self.radio_manager.radio.send(joke) - - ########### commands with arguments ########### - - def shutdown(self, cubesat: Satellite, args: bytes) -> None: - # make shutdown require yet another pass-code - if args != b"\x0b\xfdI\xec": - return - - # This means args does = b"\x0b\xfdI\xec" - self.logger.info("valid shutdown command received") - # set shutdown NVM bit flag - cubesat.f_shtdwn.toggle(True) - - """ - Exercise for the user: - Implement a means of waking up from shutdown - See beep-sat guide for more details - https://pycubed.org/resources - """ - - # deep sleep + listen - # TODO config radio - self.radio_manager.radio.listen() - if "st" in cubesat.radio_cfg: - _t: float = cubesat.radio_cfg["st"] - else: - _t = 5 - import alarm - - time_alarm: circuitpython_typing.Alarm = alarm.time.TimeAlarm( - monotonic_time=time.monotonic() + eval("1e" + str(_t)) - ) # default 1 day - # set hot start flag right before sleeping - cubesat.f_hotstrt.toggle(True) - alarm.exit_and_deep_sleep_until_alarms(time_alarm) - - def query(self, cubesat: Satellite, args: str) -> None: - self.logger.info("Sending query with args", args=args) - - self.radio_manager.radio.send(data=str(eval(args))) - - def exec_cmd(self, cubesat: Satellite, args: str) -> None: - self.logger.info("Executing command", args=args) - exec(args) diff --git a/lib/pysquared/detumble.py b/lib/pysquared/detumble.py deleted file mode 100644 index 541bc97..0000000 --- a/lib/pysquared/detumble.py +++ /dev/null @@ -1,23 +0,0 @@ -def dot_product(vector1: tuple, vector2: tuple) -> float: - return sum([a * b for a, b in zip(vector1, vector2)]) - - -def x_product(vector1: tuple, vector2: tuple) -> list: - return [ - vector1[1] * vector2[2] - vector1[2] * vector2[1], - vector1[0] * vector2[2] - vector1[2] * vector2[0], - vector1[0] * vector2[1] - vector1[1] * vector2[0], - ] - - -def gain_func(): - return 1.0 - - -def magnetorquer_dipole(mag_field: tuple, ang_vel: tuple) -> list: - gain = gain_func() - scalar_coef = -gain / pow(dot_product(mag_field, mag_field), 0.5) - dipole_out = x_product(mag_field, ang_vel) - for i in range(3): - dipole_out[i] *= scalar_coef - return dipole_out diff --git a/lib/pysquared/functions.py b/lib/pysquared/functions.py deleted file mode 100644 index 62379aa..0000000 --- a/lib/pysquared/functions.py +++ /dev/null @@ -1,352 +0,0 @@ -""" -This is the class that contains all of the functions for our CubeSat. -We pass the cubesat object to it for the definitions and then it executes -our will. -Authors: Nicole Maggard, Michael Pham, and Rachel Sarmiento -""" - -import gc -import random -import time - -from pysquared.config.config import Config -from pysquared.hardware.rfm9x.manager import RFM9xManager -from pysquared.logger import Logger -from pysquared.packet_manager import PacketManager -from pysquared.packet_sender import PacketSender -from pysquared.pysquared import Satellite -from pysquared.sleep_helper import SleepHelper - -try: - from typing import List, OrderedDict, Union -except Exception: - pass - - -class functions: - def __init__( - self, - cubesat: Satellite, - logger: Logger, - config: Config, - sleep_helper: SleepHelper, - radio_manager: RFM9xManager, - ) -> None: - self.cubesat: Satellite = cubesat - self.logger: Logger = logger - self.config: Config = config - self.sleep_helper = sleep_helper - self.radio_manager: RFM9xManager = radio_manager - - self.logger.info("Initializing Functionalities") - self.packet_manager: PacketManager = PacketManager( - logger=self.logger, max_packet_size=128 - ) - self.packet_sender: PacketSender = PacketSender( - self.logger, radio_manager, self.packet_manager, max_retries=3 - ) - - self.cubesat_name: str = config.cubesat_name - self.facestring: list = [None, None, None, None, None] - self.jokes: list[str] = config.jokes - self.last_battery_temp: float = config.last_battery_temp - self.sleep_duration: int = config.sleep_duration - self.callsign: str = config.callsign - self.state_of_health_part1: bool = False - - """ - Satellite Management Functions - """ - - def listen_loiter(self) -> None: - self.logger.debug("Listening for 10 seconds") - self.cubesat.watchdog_pet() - self.radio_manager.radio.receive_timeout = 10 - self.listen() - self.cubesat.watchdog_pet() - - self.logger.debug("Sleeping for 20 seconds") - self.cubesat.watchdog_pet() - self.sleep_helper.safe_sleep(self.sleep_duration) - self.cubesat.watchdog_pet() - - """ - Radio Functions - """ - - def send(self, msg: Union[str, bytearray]) -> None: - """Calls the RFM9x to send a message. Currently only sends with default settings. - - Args: - msg (String,Byte Array): Pass the String or Byte Array to be sent. - """ - message: str = f"{self.callsign} " + str(msg) + f" {self.callsign}" - self.radio_manager.beacon_radio_message(message) - if self.cubesat.is_licensed: - self.logger.debug("Sent Packet", packet_message=message) - else: - self.logger.warning("Failed to send packet") - - def send_packets(self, data: Union[str, bytearray]) -> None: - """Sends packets of data over the radio with delay between packets. - - Args: - data (String, Byte Array): Pass the data to be sent. - delay (float): Delay in seconds between packets - """ - self.packet_sender.send_data(data) - - def beacon(self) -> None: - """Calls the RFM9x to send a beacon.""" - - try: - lora_beacon: str = ( - f"{self.callsign} Hello I am {self.cubesat_name}! I am: " - + str(self.cubesat.power_mode) - + f" UT:{self.cubesat.get_system_uptime} BN:{self.cubesat.boot_count.get()} EC:{self.logger.get_error_count()} " - + f"IHBPFJASTMNE! {self.callsign}" - ) - except Exception as e: - self.logger.error("Error with obtaining power data: ", e) - - lora_beacon: str = ( - f"{self.callsign} Hello I am Yearling^2! I am in: " - + "an unidentified" - + " power mode. V_Batt = " - + "Unknown" - + f". IHBPFJASTMNE! {self.callsign}" - ) - - self.radio_manager.beacon_radio_message(lora_beacon) - - def joke(self) -> None: - self.send(random.choice(self.jokes)) - - def format_state_of_health(self, hardware: OrderedDict[str, bool]) -> str: - to_return: str = "" - for key, value in hardware.items(): - to_return = to_return + key + "=" - if value: - to_return += "1" - else: - to_return += "0" - - if len(to_return) > 245: - return to_return - - return to_return - - def state_of_health(self) -> None: - self.state_list: list = [] - # list of state information - try: - self.state_list: list[str] = [ - f"PM:{self.cubesat.power_mode}", - f"VB:{self.cubesat.battery_voltage}", - f"ID:{self.cubesat.current_draw}", - f"IC:{self.cubesat.charge_current}", - f"UT:{self.cubesat.get_system_uptime}", - f"BN:{self.cubesat.boot_count.get()}", - f"MT:{self.cubesat.micro.cpu.temperature}", - f"RT:{self.radio_manager.get_temperature()}", - f"AT:{self.cubesat.internal_temperature}", - f"BT:{self.last_battery_temp}", - f"EC:{self.logger.get_error_count()}", - f"AB:{int(self.cubesat.f_burned.get())}", - f"BO:{int(self.cubesat.f_brownout.get())}", - f"FK:{self.radio_manager.get_modulation()}", - ] - except Exception as e: - self.logger.error("Couldn't aquire data for the state of health: ", e) - - message: str = "" - if not self.state_of_health_part1: - message = ( - f"{self.callsign} Yearling^2 State of Health 1/2" - + str(self.state_list) - + f"{self.callsign}" - ) - self.state_of_health_part1: bool = True - else: - message = ( - f"{self.callsign} YSOH 2/2" - + self.format_state_of_health(self.cubesat.hardware) - + f"{self.callsign}" - ) - self.state_of_health_part1: bool = False - - self.radio_manager.beacon_radio_message(message) - - def send_face(self) -> None: - """Calls the data transmit function from the radio manager class""" - - self.logger.debug("Sending Face Data") - self.radio_manager.beacon_radio_message( - f"{self.callsign} Y-: {self.facestring[0]} Y+: {self.facestring[1]} X-: {self.facestring[2]} X+: {self.facestring[3]} Z-: {self.facestring[4]} {self.callsign}" - ) - - def listen(self) -> bool: - # need to instanciate cdh to feed it the config var - # assigned from the Config object - from pysquared.cdh import CommandDataHandler - - cdh = CommandDataHandler(self.config, self.logger, self.radio_manager) - - # This just passes the message through. Maybe add more functionality later. - try: - self.logger.debug("Listening") - self.radio_manager.radio.receive_timeout = 10 - received: bytearray = self.radio_manager.radio.receive_with_ack( - keep_listening=True - ) - except Exception as e: - self.logger.error("An Error has occured while listening: ", e) - received = None - - try: - if received is not None: - self.logger.debug("Received Packet", packet=received) - cdh.message_handler(self.cubesat, received) - return True - except Exception as e: - self.logger.error("An Error has occured while handling a command: ", e) - finally: - del cdh - - return False - - def listen_joke(self) -> bool: - try: - self.logger.debug("Listening") - self.radio_manager.radio.receive_timeout = 10 - received: bytearray = self.radio_manager.radio.receive(keep_listening=True) - return received is not None and "HAHAHAHAHA!" in received - - except Exception as e: - self.logger.error("An Error has occured while listening for a joke", e) - return False - - """ - Big_Data Face Functions - change to remove fet values, move to pysquared - """ - - def all_face_data(self) -> list: - # self.cubesat.all_faces_on() - gc.collect() - self.logger.debug( - "Free Memory Stat at beginning of all_face_data function", - bytes_free=gc.mem_free(), - ) - - try: - import pysquared.Big_Data as Big_Data - - self.logger.debug( - "Free Memory Stat after importing Big_data library", - bytes_free=gc.mem_free(), - ) - - gc.collect() - a: Big_Data.AllFaces = Big_Data.AllFaces(self.cubesat.tca, self.logger) - self.logger.debug( - "Free Memory Stat after initializing All Faces object", - bytes_free=gc.mem_free(), - ) - - self.facestring: list[list[float]] = a.face_test_all() - - del a - del Big_Data - gc.collect() - - except Exception as e: - self.logger.error("Big_Data error", e) - - return self.facestring - - def get_imu_data( - self, - ) -> List[ - tuple[float, float, float], - tuple[float, float, float], - tuple[float, float, float], - ]: - try: - data: List[ - tuple[float, float, float], - tuple[float, float, float], - tuple[float, float, float], - ] = [] - data.append(self.cubesat.accel) - data.append(self.cubesat.gyro) - data.append(self.cubesat.mag) - except Exception as e: - self.logger.error("Error retrieving IMU data", e) - - return data - - def OTA(self) -> None: - # resets file system to whatever new file is received - self.logger.debug("Implement an OTA Function Here") - pass - - """ - Misc Functions - """ - - # Goal for torque is to make a control system - # that will adjust position towards Earth based on Gyro data - def detumble(self, dur: int = 7) -> None: - self.logger.debug("Detumbling") - self.cubesat.rgb = (255, 255, 255) - - try: - import pysquared.Big_Data as Big_Data - - a: Big_Data.AllFaces = Big_Data.AllFaces(self.cubesat.tca, self.logger) - except Exception as e: - self.logger.error("Error Importing Big Data", e) - - try: - a.sequence = 52 - except Exception as e: - self.logger.error("Error setting motor driver sequences", e) - - def actuate(dipole: list[float], duration) -> None: - # TODO figure out if there is a way to reverse direction of sequence - if abs(dipole[0]) > 1: - a.Face2.drive = 52 - a.drvx_actuate(duration) - if abs(dipole[1]) > 1: - a.Face0.drive = 52 - a.drvy_actuate(duration) - if abs(dipole[2]) > 1: - a.Face4.drive = 52 - a.drvz_actuate(duration) - - def do_detumble() -> None: - try: - import pysquared.detumble as detumble - - for _ in range(3): - data = [self.cubesat.IMU.Gyroscope, self.cubesat.IMU.Magnetometer] - data[0] = list(data[0]) - for x in range(3): - if data[0][x] < 0.01: - data[0][x] = 0.0 - data[0] = tuple(data[0]) - dipole = detumble.magnetorquer_dipole(data[1], data[0]) - self.logger.debug("Detumbling", dipole=dipole) - self.send("Detumbling! Gyro, Mag: " + str(data)) - time.sleep(1) - actuate(dipole, dur) - except Exception as e: - self.logger.error("Detumble error", e) - - try: - self.logger.debug("Attempting") - do_detumble() - except Exception as e: - self.logger.error("Detumble error", e) - self.cubesat.rgb = (100, 100, 50) diff --git a/lib/pysquared/logger.py b/lib/pysquared/logger.py deleted file mode 100644 index af01f70..0000000 --- a/lib/pysquared/logger.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -Logger class for handling logging messages with different severity levels. -Logs can be output to standard output or saved to a file (functionality to be implemented). -""" - -import json -import time -import traceback -from collections import OrderedDict - -from pysquared.nvm.counter import Counter - - -def _color(msg, color="gray", fmt="normal"): - _h = "\033[" - _e = "\033[0;39;49m" - - _c = { - "red": "1", - "green": "2", - "orange": "3", - "blue": "4", - "pink": "5", - "teal": "6", - "white": "7", - "gray": "9", - } - - _f = {"normal": "0", "bold": "1", "ulined": "4"} - return _h + _f[fmt] + ";3" + _c[color] + "m" + msg + _e - - -LogColors = { - "NOTSET": "NOTSET", - "DEBUG": _color(msg="DEBUG", color="blue"), - "INFO": _color(msg="INFO", color="green"), - "WARNING": _color(msg="WARNING", color="orange"), - "ERROR": _color(msg="ERROR", color="pink"), - "CRITICAL": _color(msg="CRITICAL", color="red"), -} - - -class LogLevel: - NOTSET = 0 - DEBUG = 1 - INFO = 2 - WARNING = 3 - ERROR = 4 - CRITICAL = 5 - - -class Logger: - def __init__( - self, - error_counter: Counter, - log_level: int = LogLevel.NOTSET, - colorized: bool = False, - ) -> None: - self._error_counter: Counter = error_counter - self._log_level: int = log_level - self.colorized: bool = colorized - - def _can_print_this_level(self, level_value: int) -> bool: - return level_value >= self._log_level - - def _log(self, level: str, level_value: int, message: str, **kwargs) -> None: - """ - Log a message with a given severity level and any addional key/values. - """ - now = time.localtime() - asctime = f"{now.tm_year}-{now.tm_mon:02d}-{now.tm_mday:02d} {now.tm_hour:02d}:{now.tm_min:02d}:{now.tm_sec:02d}" - - # case where someone used debug, info, or warning yet also provides an 'err' kwarg with an Exception - if ( - "err" in kwargs - and level not in ("ERROR", "CRITICAL") - and isinstance(kwargs["err"], Exception) - ): - kwargs["err"] = traceback.format_exception(kwargs["err"]) - - json_order: OrderedDict[str, str] = OrderedDict( - [("time", asctime), ("level", level), ("msg", message)] - ) - json_order.update(kwargs) - - try: - json_output = json.dumps(json_order) - except TypeError as e: - json_output = json.dumps( - OrderedDict( - [ - ("time", asctime), - ("level", "ERROR"), - ("msg", f"Failed to serialize log message: {e}"), - ] - ), - ) - - if self._can_print_this_level(level_value): - if self.colorized: - json_output = json_output.replace( - f'"level": "{level}"', f'"level": "{LogColors[level]}"' - ) - print(json_output) - - def debug(self, message: str, **kwargs) -> None: - """ - Log a message with severity level DEBUG. - """ - self._log("DEBUG", 1, message, **kwargs) - - def info(self, message: str, **kwargs) -> None: - """ - Log a message with severity level INFO. - """ - self._log("INFO", 2, message, **kwargs) - - def warning(self, message: str, **kwargs) -> None: - """ - Log a message with severity level WARNING. - """ - self._log("WARNING", 3, message, **kwargs) - - def error(self, message: str, err: Exception, **kwargs) -> None: - """ - Log a message with severity level ERROR. - """ - kwargs["err"] = traceback.format_exception(err) - self._error_counter.increment() - self._log("ERROR", 4, message, **kwargs) - - def critical(self, message: str, err: Exception, **kwargs) -> None: - """ - Log a message with severity level CRITICAL. - """ - kwargs["err"] = traceback.format_exception(err) - self._error_counter.increment() - self._log("CRITICAL", 5, message, **kwargs) - - def get_error_count(self) -> int: - return self._error_counter.get() diff --git a/lib/pysquared/packet_manager.py b/lib/pysquared/packet_manager.py deleted file mode 100644 index 4e61a12..0000000 --- a/lib/pysquared/packet_manager.py +++ /dev/null @@ -1,137 +0,0 @@ -# Written with Claude 3.5 -# Nov 10, 2024 -from pysquared.logger import Logger - -try: - from typing import Union -except Exception: - pass - - -class PacketManager: - def __init__(self, logger: Logger, max_packet_size: int = 128) -> None: - """Initialize the packet manager with maximum packet size (default 128 bytes for typical LoRa)""" - self.max_packet_size: int = max_packet_size - self.header_size: int = 4 # 2 bytes for sequence number, 2 for total packets - self.payload_size: int = max_packet_size - self.header_size - self.logger: Logger = logger - - def create_retransmit_request(self, missing_packets: list[int]) -> bytes: - """ - Create a packet requesting retransmission - Format: - - 2 bytes: 0xFFFF (special sequence number indicating retransmit request) - - 2 bytes: Number of missing packets - - Remaining bytes: Missing packet sequence numbers - """ - header: bytes = b"\xff\xff" + len(missing_packets).to_bytes(2, "big") - payload: bytes = b"".join( - sequence_number.to_bytes(2, "big") for sequence_number in missing_packets - ) - return header + payload - - def is_retransmit_request(self, packet: bytes) -> bool: - """Check if packet is a retransmit request""" - return len(packet) >= 4 and packet[:2] == b"\xff\xff" - - def parse_retransmit_request(self, packet: bytes) -> list[int]: - """Extract missing packet numbers from retransmit request""" - num_missing: int = int.from_bytes(packet[2:4], "big") - missing: list[int] = [] - for i in range(num_missing): - start_idx: int = 4 + (i * 2) - sequence_number: int = int.from_bytes( - packet[start_idx : start_idx + 2], "big" - ) - missing.append(sequence_number) - return missing - - def pack_data(self, data) -> list[bytes]: - """ - Takes input data and returns a list of packets ready for transmission - Each packet includes: - - 2 bytes: sequence number (0-based) - - 2 bytes: total number of packets - - remaining bytes: payload - """ - # Convert data to bytes if it isn't already - if not isinstance(data, bytes): - if isinstance(data, str): - data: bytes = data.encode("utf-8") - else: - data: bytes = str(data).encode("utf-8") - - # Calculate number of packets needed - total_packets: int = (len(data) + self.payload_size - 1) // self.payload_size - self.logger.info( - "Packing data into packets", - num_packets=total_packets, - data_length=len(data), - ) - - packets: list[bytes] = [] - for sequence_number in range(total_packets): - # Create header - header: bytes = sequence_number.to_bytes(2, "big") + total_packets.to_bytes( - 2, "big" - ) - self.logger.info("Created header", header=[hex(b) for b in header]) - - # Get payload slice for this packet - start: int = sequence_number * self.payload_size - end: int = start + self.payload_size - payload: bytes = data[start:end] - - # Combine header and payload - packet: bytes = header + payload - self.logger.info( - "Combining the header and payload to form a Packet", - packet=sequence_number, - packet_length=len(packet), - header=[hex(b) for b in header], - ) - packets.append(packet) - - return packets - - def unpack_data(self, packets: list) -> Union[bytes, None]: - """ - Takes a list of packets and reassembles the original data - Returns None if packets are missing or corrupted - """ - if not packets: - return None - - # Sort packets by sequence number - try: - packets: list = sorted(packets, key=lambda p: int.from_bytes(p[:2], "big")) - except Exception: - return None - - # Verify all packets are present - total_packets: int = int.from_bytes(packets[0][2:4], "big") - if len(packets) != total_packets: - return None - - # Verify sequence numbers are consecutive - for i, packet in enumerate(packets): - if int.from_bytes(packet[:2], "big") != i: - return None - - # Combine payloads - data: bytes = b"".join(packet[self.header_size :] for packet in packets) - return data - - def create_ack_packet(self, sequence_number: int) -> bytes: - """Creates an acknowledgment packet for a given sequence number""" - return b"ACK" + sequence_number.to_bytes(2, "big") - - def is_ack_packet(self, packet: str) -> bool: - """Checks if a packet is an acknowledgment packet""" - return packet.startswith(b"ACK") - - def get_ack_seq_num(self, ack_packet: str) -> Union[int, None]: - """Extracts sequence number from an acknowledgment packet""" - if self.is_ack_packet(ack_packet): - return int.from_bytes(ack_packet[3:5], "big") - return None diff --git a/lib/pysquared/packet_sender.py b/lib/pysquared/packet_sender.py deleted file mode 100644 index 1d564af..0000000 --- a/lib/pysquared/packet_sender.py +++ /dev/null @@ -1,213 +0,0 @@ -from pysquared.hardware.rfm9x.manager import RFM9xManager -from pysquared.logger import Logger -from pysquared.packet_manager import PacketManager - -try: - from typing import Union -except Exception: - pass - - -class PacketSender: - def __init__( - self, - logger: Logger, - radio_manager: RFM9xManager, - packet_manager: PacketManager, - ack_timeout: float = 2.0, - max_retries: int = 3, - send_delay: float = 0.2, - ) -> None: - """ - Initialize the packet sender with optimized timing - """ - self.logger: Logger = logger - self.radio_manager: RFM9xManager = radio_manager - self.packet_manager: PacketManager = packet_manager - self.ack_timeout: float = ack_timeout - self.max_retries: int = max_retries - self.send_delay: float = send_delay - - def wait_for_ack(self, expected_seq: int) -> bool: - """ - Optimized ACK wait with early return - """ - import time - - start_time: float = time.monotonic() - - # Minimal delay after sending - time.sleep(self.send_delay) - - while (time.monotonic() - start_time) < self.ack_timeout: - packet: bytearray = self.radio_manager.radio.receive() - - if packet and self.packet_manager.is_ack_packet(packet): - ack_seq: Union[int, None] = self.packet_manager.get_ack_seq_num(packet) - if ack_seq == expected_seq: - # Got our ACK - only wait briefly for a duplicate then continue - time.sleep(0.2) - return True - - time.sleep(0.1) # Small delay between checks - - return False - - def send_packet_with_retry(self, packet: bytes, seq_num: int) -> bool: - """Optimized packet sending with minimal delays""" - import time - - for attempt in range(self.max_retries): - self.radio_manager.radio.send(packet) - - if self.wait_for_ack(seq_num): - # Success - minimal delay before next packet - time.sleep(0.2) - return True - - if attempt < self.max_retries - 1: - # Only short delay before retry - time.sleep(1.0) - - return False - - def send_data( - self, data: Union[str, bytearray], progress_interval: int = 10 - ) -> bool: - """Send data with minimal progress updates""" - packets: list[bytes] = self.packet_manager.pack_data(data) - total_packets: int = len(packets) - self.logger.info("Sending packets...", num_packets=total_packets) - - for i, packet in enumerate(packets): - if i % progress_interval == 0: - self.logger.info( - "Making progress sending packets", - current_packet=i, - num_packets=total_packets, - ) - - if not self.send_packet_with_retry(packet, i): - self.logger.warning( - "Failed to send packet", current_packet=i, num_packets=total_packets - ) - return False - - self.logger.info( - "Successfully sent all the packets!", num_packets=total_packets - ) - return True - - def handle_retransmit_request( - self, packets: list[bytes], request_packet: list[str] - ) -> bool: - """Handle retransmit request by sending requested packets""" - import time - - try: - missing_packets: list[int] = self.packet_manager.parse_retransmit_request( - request_packet - ) - self.logger.info( - "Retransmit request received for missing packets", - num_missing_packets=len(missing_packets), - ) - time.sleep(0.2) # Small delay before retransmission - - for seq in missing_packets: - if seq < len(packets): - self.logger.info("Retransmitting packet ", packet=seq) - self.radio_manager.radio.send(packets[seq]) - time.sleep(0.2) # Small delay between retransmitted packets - self.radio_manager.radio.send(packets[seq]) - time.sleep(0.2) # Small delay between retransmitted packets - - return True - - except Exception as e: - self.logger.error("Error handling retransmit request", e) - return False - - def fast_send_data( - self, - data: Union[str, bytearray], - send_delay: float = 0.5, - retransmit_wait: float = 15.0, - ) -> bool: - """Send data with improved retransmission handling""" - import time - - packets: list[bytes] = self.packet_manager.pack_data(data) - total_packets: int = len(packets) - self.logger.info("Sending packets..", num_packets=total_packets) - - # Send first packet with retry until ACKed - for attempt in range(self.max_retries): - self.logger.info( - "Sending first packet", - attempt_num=attempt + 1, - max_retries=self.max_retries, - ) - self.radio_manager.radio.send(packets[0]) - - if self.wait_for_ack(0): - break - else: - if attempt < self.max_retries - 1: - time.sleep(1.0) - else: - self.logger.warning("Failed to get ACK for first packet") - return False - - # Send remaining packets without waiting for ACKs - self.logger.info("Sending remaining packets...") - for i in range(1, total_packets): - if i % 10 == 0: - self.logger.info( - "Sending packet", current_packet=i, num_packets=total_packets - ) - self.radio_manager.radio.send(packets[i]) - time.sleep(send_delay) - - self.logger.info("Waiting for retransmit requests...") - retransmit_end_time: float = time.monotonic() + retransmit_wait - - while time.monotonic() < retransmit_end_time: - packet: bytearray = self.radio_manager.radio.receive() - if not packet: - break - - self.logger.info( - "Received potential retransmit request:", - packet=[hex(b) for b in packet], - ) - - if not self.packet_manager.is_retransmit_request(packet): - break - - self.logger.info("Valid retransmit request received!") - missing_packets = self.packet_manager.parse_retransmit_request(packet) - self.logger.info("Retransmitting packets", missing_packets=missing_packets) - - # Add delay before retransmission to let receiver get ready - time.sleep(1) - - for seq in missing_packets: - if seq >= len(packets): - break - - self.logger.info("Retransmitting packet", packet=seq) - self.radio_manager.radio.send(packets[seq]) - time.sleep(0.5) # Longer delay between retransmitted packets - self.logger.info("Retransmitting packet", packet=seq) - self.radio_manager.radio.send(packets[seq]) - time.sleep(0.2) # Longer delay between retransmitted packets - - # Reset timeout and add extra delay after retransmission - time.sleep(1.0) - retransmit_end_time: float = time.monotonic() + retransmit_wait - - time.sleep(0.1) - - self.logger.info("Finished sending all packets") - return True diff --git a/lib/pysquared/pysquared.py b/lib/pysquared/pysquared.py deleted file mode 100644 index 5dda946..0000000 --- a/lib/pysquared/pysquared.py +++ /dev/null @@ -1,726 +0,0 @@ -""" -CircuitPython driver for PySquared satellite board. -PySquared Hardware Version: Flight Controller V4c -CircuitPython Version: 9.0.0 -Library Repo: - -* Author(s): Nicole Maggard, Michael Pham, and Rachel Sarmiento -""" - -# Common CircuitPython Libs -import sys -import time -from collections import OrderedDict -from os import chdir, mkdir, stat - -import board -import busio -import digitalio -import microcontroller -import sdcardio -from micropython import const -from storage import VfsFat, mount, umount - -import lib.adafruit_lis2mdl as adafruit_lis2mdl # Magnetometer -import lib.adafruit_tca9548a as adafruit_tca9548a # I2C Multiplexer -import lib.neopixel as neopixel # RGB LED -import lib.rv3028.rv3028 as rv3028 # Real Time Clock -import pysquared.nvm.register as register -from lib.adafruit_lsm6ds.lsm6dsox import LSM6DSOX # IMU -from pysquared.config.config import Config # Configs -from pysquared.nvm.counter import Counter -from pysquared.nvm.flag import Flag - -try: - from typing import Any, Callable, Optional, OrderedDict, TextIO, Union - - import circuitpython_typing -except Exception: - pass - -from pysquared.logger import Logger - -SEND_BUFF: bytearray = bytearray(252) - - -class Satellite: - """ - NVM (Non-Volatile Memory) Register Definitions - """ - - # General NVM counters - boot_count: Counter = Counter(index=register.BOOTCNT, datastore=microcontroller.nvm) - - # Define NVM flags - f_softboot: Flag = Flag( - index=register.FLAG, bit_index=0, datastore=microcontroller.nvm - ) - f_brownout: Flag = Flag( - index=register.FLAG, bit_index=3, datastore=microcontroller.nvm - ) - f_shtdwn: Flag = Flag( - index=register.FLAG, bit_index=5, datastore=microcontroller.nvm - ) - f_burned: Flag = Flag( - index=register.FLAG, bit_index=6, datastore=microcontroller.nvm - ) - - def safe_init(func: Callable[..., Any]): - def wrapper(self, *args, **kwargs): - hardware_key: str = kwargs.get("hardware_key", "UNKNOWN") - self.logger.debug( - "Initializing hardware component", hardware_key=hardware_key - ) - - try: - device: Any = func(self, *args, **kwargs) - return device - - except Exception as e: - self.logger.error( - "There was an error initializing this hardware component", - e, - hardware_key=hardware_key, - ) - return None - - return wrapper - - @safe_init - def init_general_hardware( - self, - init_func: Callable[..., Any], - *args: Any, - hardware_key, - orpheus_func: Callable[..., Any] = None, - **kwargs: Any, - ) -> Any: - """ - Args: - init_func (Callable[..., Any]): The function used to initialize the hardware. - *args (Any): Positional arguments to pass to the `init_func`. - hardware_key (str): A unique identifier for the hardware being initialized. - orpheus_func (Callable[..., Any], optional): An alternative function to initialize - the hardware if the `orpheus` flag is set. Defaults to `None`. - **kwargs (Any): Additional keyword arguments to pass to the `init_func`. - Must be placed before `hardware_key`. - - Returns: - Any: The initialized hardware instance if successful, or `None` if an error occurs. - - Raises: - Exception: Any exception raised by the `init_func` or `orpheus_func` - will be caught and handled by the `@safe_init` decorator. - """ - if self.orpheus and orpheus_func: - return orpheus_func(hardware_key) - - hardware_instance = init_func(*args, **kwargs) - self.hardware[hardware_key] = True - return hardware_instance - - @safe_init - def init_rtc(self, hardware_key: str) -> None: - self.rtc: rv3028.RV3028 = rv3028.RV3028(self.i2c1) - - # Still need to test these configs - self.rtc.configure_backup_switchover(mode="level", interrupt=True) - self.hardware[hardware_key] = True - - @safe_init - def init_sd_card(self, hardware_key: str) -> None: - # Baud rate depends on the card, 4MHz should be safe - _sd = sdcardio.SDCard(self.spi0, board.SPI0_CS1, baudrate=4000000) - _vfs = VfsFat(_sd) - mount(_vfs, "/sd") - self.fs = _vfs - sys.path.append("/sd") - self.hardware[hardware_key] = True - - @safe_init - def init_neopixel(self, hardware_key: str) -> None: - self.neopwr: digitalio.DigitalInOut = digitalio.DigitalInOut(board.NEO_PWR) - self.neopwr.switch_to_output(value=True) - self.neopixel: neopixel.NeoPixel = neopixel.NeoPixel( - board.NEOPIX, 1, brightness=0.2, pixel_order=neopixel.GRB - ) - self.neopixel[0] = (0, 0, 255) - self.hardware[hardware_key] = True - - @safe_init - def init_tca_multiplexer(self, hardware_key: str) -> None: - try: - self.tca: adafruit_tca9548a.TCA9548A = adafruit_tca9548a.TCA9548A( - self.i2c1, address=int(0x77) - ) - self.hardware[hardware_key] = True - except OSError: - self.logger.error( - "TCA try_lock failed. TCA may be malfunctioning.", - hardware_key=hardware_key, - ) - self.hardware[hardware_key] = False - return - - def __init__(self, config: Config, logger: Logger, version: str) -> None: - self.config: Config = config - self.cubesat_name: str = config.cubesat_name - """ - Big init routine as the whole board is brought up. Starting with config variables. - """ - self.legacy: bool = config.legacy - self.heating: bool = config.heating - self.orpheus: bool = config.orpheus # maybe change var name - self.is_licensed: bool = config.is_licensed - self.logger = logger - - """ - Define the normal power modes - """ - self.normal_temp: int = config.normal_temp - self.normal_battery_temp: int = config.normal_battery_temp - self.normal_micro_temp: int = config.normal_micro_temp - self.normal_charge_current: float = config.normal_charge_current - self.normal_battery_voltage: float = config.normal_battery_voltage - self.critical_battery_voltage: float = config.critical_battery_voltage - self.battery_voltage: float = config.battery_voltage - self.current_draw: float = config.current_draw - self.reboot_time: int = config.reboot_time - self.turbo_clock: bool = config.turbo_clock - - """ - Setting up data buffers - """ - # TODO(cosmiccodon/blakejameson): - # Data_cache, filenumbers, image_packets, and send_buff are variables that are not used in the codebase. They were put here for Orpheus last minute. - # We are unsure if these will be used in the future, so we are keeping them here for now. - self.data_cache: dict = {} - self.filenumbers: dict = {} - self.image_packets: int = 0 - self.uart_baudrate: int = 9600 - self.buffer: Optional[bytearray] = None - self.buffer_size: int = 1 - self.send_buff: memoryview = memoryview(SEND_BUFF) - self.micro: microcontroller = microcontroller - - # Confused here, as self.battery_voltage was initialized to 3.3 in line 113(blakejameson) - # NOTE(blakejameson): After asking Michael about the None variables below last night at software meeting, he mentioned they used - # None as a state instead of the values to better manage some conditions with Orpheus. - # I need to get a better understanding for the values and flow before potentially refactoring code here. - self.battery_voltage: Optional[float] = None - self.draw_current: Optional[float] = None - self.charge_voltage: Optional[float] = None - self.charge_current: Optional[float] = None - self.is_charging: Optional[bool] = None - self.battery_percentage: Optional[float] = None - - """ - Define the boot time and current time - """ - - self.BOOTTIME = time.time() - self.logger.debug("Booting up!", boot_time=f"{self.BOOTTIME}s") - self.CURRENTTIME: int = self.BOOTTIME - self.UPTIME: int = 0 - - self.hardware: OrderedDict[str, bool] = OrderedDict( - [ - ("I2C0", False), - ("SPI0", False), - ("I2C1", False), - ("UART", False), - ("IMU", False), - ("Mag", False), - ("SDcard", False), - ("NEOPIX", False), - ("WDT", False), - ("TCA", False), - ("Face0", False), - ("Face1", False), - ("Face2", False), - ("Face3", False), - ("Face4", False), - ("RTC", False), - ] - ) - - if self.f_softboot.get(): - self.f_softboot.toggle(False) - - """ - Setting up the watchdog pin. - """ - - self.watchdog_pin: digitalio.DigitalInOut = digitalio.DigitalInOut( - board.WDT_WDI - ) - self.watchdog_pin.direction = digitalio.Direction.OUTPUT - self.watchdog_pin.value = False - - """ - Set the CPU Clock Speed - """ - cpu_freq: int = 125000000 if self.turbo_clock else 62500000 - for cpu in microcontroller.cpus: - cpu.frequency = cpu_freq - - """ - Intializing Communication Buses - """ - - # Alternative Implementations of hardware initialization specific for orpheus - def orpheus_skip_i2c(hardware_key: str) -> None: - self.logger.debug( - "Hardware component not initialized", - cubesat=self.cubesat_name, - hardware_key=hardware_key, - ) - return None - - def orpheus_init_uart(hardware_key: str): - uart: circuitpython_typing.ByteStream = busio.UART( - board.I2C0_SDA, board.I2C0_SCL, baudrate=self.uart_baudrate - ) - self.hardware[hardware_key] = True - return uart - - self.i2c0: busio.I2C = self.init_general_hardware( - busio.I2C, - board.I2C0_SCL, - board.I2C0_SDA, - hardware_key="I2C0", - orpheus_func=orpheus_skip_i2c, - ) - - self.spi0: busio.SPI = self.init_general_hardware( - busio.SPI, - board.SPI0_SCK, - board.SPI0_MOSI, - board.SPI0_MISO, - hardware_key="SPI0", - ) - - self.i2c1: busio.I2C = self.init_general_hardware( - busio.I2C, - board.I2C1_SCL, - board.I2C1_SDA, - frequency=100000, - hardware_key="I2C1", - ) - - self.uart: circuitpython_typing.ByteStream = self.init_general_hardware( - busio.UART, - board.TX, - board.RX, - baud_rate=self.uart_baudrate, - hardware_key="UART", - orpheus_func=orpheus_init_uart, - ) - - ######## Temporary Fix for RF_ENAB ######## - # # - if self.legacy: - self.enable_rf: digitalio.DigitalInOut = digitalio.DigitalInOut( - board.RF_ENAB - ) - # self.enable_rf.switch_to_output(value=False) # if U21 - self.enable_rf.switch_to_output(value=True) # if U7 - else: - self.enable_rf: bool = True - # # - ######## Temporary Fix for RF_ENAB ######## - - self.imu: LSM6DSOX = self.init_general_hardware( - LSM6DSOX, i2c_bus=self.i2c1, address=0x6B, hardware_key="IMU" - ) - self.mangetometer: adafruit_lis2mdl.LIS2MDL = self.init_general_hardware( - adafruit_lis2mdl.LIS2MDL, self.i2c1, hardware_key="Mag" - ) - self.init_rtc(hardware_key="RTC") - self.init_sd_card(hardware_key="SD Card") - self.init_neopixel(hardware_key="NEOPIX") - self.init_tca_multiplexer(hardware_key="TCA") - - """ - Face Initializations - """ - self.scan_tca_channels() - - """ - Prints init State of PySquared Hardware - """ - self.logger.debug("PySquared Hardware Initialization Complete!") - - for key, value in self.hardware.items(): - if value: - self.logger.info( - "Successfully initialized hardware device", - device=key, - status=True, - ) - else: - self.logger.warning( - "Unable to initialize hardware device", device=key, status=False - ) - # set power mode - self.power_mode: str = "normal" - - # Set current version - self.version: str = version - - """ - Init Helper Functions - """ - - def scan_tca_channels(self) -> None: - if not self.hardware["TCA"]: - self.logger.warning("TCA not initialized") - return - - channel_to_face: dict[int, str] = { - 0: "Face0", - 1: "Face1", - 2: "Face2", - 3: "Face3", - 4: "Face4", - } - - for channel in range(len(channel_to_face)): - try: - self._scan_single_channel(channel, channel_to_face) - except OSError as os_error: - self.logger.error( - "TCA try_lock failed. TCA may be malfunctioning.", os_error - ) - self.hardware["TCA"] = False - return - except Exception as e: - self.logger.error( - "There was an Exception during the scan_tca_channels function call", - e, - face=channel_to_face[channel], - ) - - def _scan_single_channel( - self, channel: int, channel_to_face: dict[int, str] - ) -> None: - if not self.tca[channel].try_lock(): - return - - try: - addresses: list[int] = self.tca[channel].scan() - valid_addresses: list[int] = [ - addr for addr in addresses if addr not in [0x00, 0x19, 0x1E, 0x6B, 0x77] - ] - - if not valid_addresses and 0x77 in addresses: - self.logger.error( - "No Devices Found on channel", channel=channel_to_face[channel] - ) - self.hardware[channel_to_face[channel]] = False - else: - self.logger.debug( - channel=channel, - valid_addresses=[hex(addr) for addr in valid_addresses], - ) - if channel in channel_to_face: - self.hardware[channel_to_face[channel]] = True - except Exception as e: - self.logger.error( - "There was an Exception during the _scan_single_channel function call", - e, - face=channel_to_face[channel], - ) - finally: - self.tca[channel].unlock() - - """ - Code to call satellite parameters - """ - - @property - def rgb(self) -> tuple[int, int, int]: - return self.neopixel[0] - - @rgb.setter - def rgb(self, value: tuple[int, int, int]) -> None: - if not self.hardware["NEOPIX"]: - self.logger.warning("The NEOPIXEL device is not initialized") - return - - # NEOPIX is initialized - try: - self.neopixel[0] = value - except Exception as e: - self.logger.error( - "There was an error trying to set the new RGB value", - e, - value=value, - ) - - @property - def get_system_uptime(self) -> int: - self.CURRENTTIME: int = const(time.time()) - return self.CURRENTTIME - self.BOOTTIME - - @property - def reset_vbus(self) -> None: - # unmount SD card to avoid errors - if self.hardware["SDcard"]: - try: - umount("/sd") - time.sleep(3) - except Exception as e: - self.logger.error("There was an error unmounting the SD card", e) - try: - self.logger.debug( - "Resetting VBUS [IMPLEMENT NEW FUNCTION HERE]", - ) - except Exception as e: - self.logger.error("There was a vbus reset error", e) - - @property - def gyro(self) -> Union[tuple[float, float, float], None]: - try: - return self.imu.gyro - except Exception as e: - self.logger.error("There was an error retrieving the gyro values", e) - - @property - def accel(self) -> Union[tuple[float, float, float], None]: - try: - return self.imu.acceleration - except Exception as e: - self.logger.error( - "There was an error retrieving the accelerometer values", e - ) - - @property - def internal_temperature(self) -> Union[float, None]: - try: - return self.imu.temperature - except Exception as e: - self.logger.error( - "There was an error retrieving the internal temperature value", e - ) - - @property - def mag(self) -> Union[tuple[float, float, float], None]: - try: - return self.mangetometer.magnetic - except Exception as e: - self.logger.error( - "There was an error retrieving the magnetometer sensor values", e - ) - - @property - def time(self) -> Union[tuple[int, int, int], None]: - try: - return self.rtc.get_time() - except Exception as e: - self.logger.error("There was an error retrieving the RTC time", e) - - @time.setter - def time(self, hms: tuple[int, int, int]) -> None: - """ - hms: A 3-tuple of ints containing data for the hours, minutes, and seconds respectively. - """ - hours, minutes, seconds = hms - if not self.hardware["RTC"]: - self.logger.warning("The RTC is not initialized") - return - - try: - self.rtc.set_time(hours, minutes, seconds) - except Exception as e: - self.logger.error( - "There was an error setting the RTC time", - e, - hms=hms, - hour=hms[0], - minutes=hms[1], - seconds=hms[2], - ) - - @property - def date(self) -> Union[tuple[int, int, int, int], None]: - try: - return self.rtc.get_date() - except Exception as e: - self.logger.error("There was an error retrieving RTC date", e) - - @date.setter - def date(self, ymdw: tuple[int, int, int, int]) -> None: - """ - ymdw: A 4-tuple of ints containing data for the year, month, date, and weekday respectively. - """ - year, month, date, weekday = ymdw - if not self.hardware["RTC"]: - self.logger.warning("RTC not initialized") - return - - try: - self.rtc.set_date(year, month, date, weekday) - except Exception as e: - self.logger.error( - "There was an error setting the RTC date", - e, - ymdw=ymdw, - year=ymdw[0], - month=ymdw[1], - date=ymdw[2], - weekday=ymdw[3], - ) - - """ - Maintenence Functions - """ - - def watchdog_pet(self) -> None: - self.watchdog_pin.value = True - time.sleep(0.01) - self.watchdog_pin.value = False - - def check_reboot(self) -> None: - self.UPTIME: int = self.get_system_uptime - self.logger.debug("Current up time stat:", uptime=self.UPTIME) - if self.UPTIME > self.reboot_time: - self.micro.reset() - - def powermode(self, mode: str) -> None: - """ - Configure the hardware for minimum or normal power consumption - Add custom modes for mission-specific control - """ - try: - if "crit" in mode: - self.neopixel.brightness = 0 - self.enable_rf.value = False - self.power_mode: str = "critical" - - elif "min" in mode: - self.neopixel.brightness = 0 - self.enable_rf.value = False - - self.power_mode: str = "minimum" - - elif "norm" in mode: - self.enable_rf.value = True - self.power_mode: str = "normal" - # don't forget to reconfigure radios, gps, etc... - - elif "max" in mode: - self.enable_rf.value = True - self.power_mode: str = "maximum" - except Exception as e: - self.logger.error( - "There was an Error in changing operations of powermode", - e, - mode=mode, - ) - - """ - SD Card Functions - """ - - def print_file(self, filedir: str = None, binary: bool = False) -> None: - try: - if filedir is None: - raise FileNotFoundError("file directory is empty") - self.logger.debug("Printing File", file_dir=filedir) - if binary: - with open(filedir, "rb") as file: - self.logger.debug( - "Printing in binary mode", content=str(file.read()) - ) - else: - with open(filedir, "r") as file: - for line in file: - self.logger.info(line.strip()) - except Exception as e: - self.logger.error( - "Can't print file", e, filedir=filedir, binary_mode=binary - ) - - def read_file( - self, filedir: str = None, binary: bool = False - ) -> Union[bytes, TextIO, None]: - try: - if filedir is None: - raise FileNotFoundError("file directory is empty") - self.logger.debug("Reading a file", file_dir=filedir) - if binary: - with open(filedir, "rb") as file: - self.logger.debug(str(file.read())) - return file.read() - else: - with open(filedir, "r") as file: - for line in file: - self.logger.debug(str(line.strip())) - return file - except Exception as e: - self.logger.error("Can't read file", e, filedir=filedir, binary_mode=binary) - - def new_file(self, substring: str, binary: bool = False) -> Union[str, None]: - """ - substring something like '/data/DATA_' - directory is created on the SD! - int padded with zeros will be appended to the last found file - """ - if not self.hardware["SDcard"]: - self.logger.warning("SD Card not initialized") - - # SDCard is initialized - try: - ff: str = "" - n: int = 0 - _folder: str = substring[: substring.rfind("/") + 1] - _file: str = substring[substring.rfind("/") + 1 :] - self.logger.debug( - "Creating new file in directory: /sd{} with file prefix: {}".format( - _folder, _file - ), - ) - try: - chdir("/sd" + _folder) - except OSError: - self.logger.error( - "The directory was not found. Now Creating...", - directory=_folder, - ) - try: - mkdir("/sd" + _folder) - except Exception as e: - self.logger.error( - "Error with creating new file", - e, - filedir="/sd" + _folder, - ) - return None - for i in range(0xFFFF): - ff: str = "/sd{}{}{:05}.txt".format(_folder, _file, (n + i) % 0xFFFF) - try: - if n is not None: - stat(ff) - except Exception as e: - self.logger.error( - "There was an error running the stat function on this file", - e, - filedir=ff, - file_num=n, - ) - n: int = (n + i) % 0xFFFF - # print('file number is',n) - break - self.logger.debug("creating a file...", file_dir=str(ff)) - if binary: - b: str = "ab" - else: - b: str = "a" - with open(ff, b) as f: - f.tell() - chdir("/") - return ff - except Exception as e: - self.logger.error("Error creating file", e, filedir=ff, binary_mode=binary) - return None diff --git a/lib/pysquared/sleep_helper.py b/lib/pysquared/sleep_helper.py deleted file mode 100644 index 5cf7b35..0000000 --- a/lib/pysquared/sleep_helper.py +++ /dev/null @@ -1,107 +0,0 @@ -import gc -import time - -import alarm -import digitalio - -from pysquared.logger import Logger -from pysquared.pysquared import Satellite - -try: - from typing import Literal - - import circuitpython_typing -except Exception: - pass - - -class SleepHelper: - """ - Class responsible for sleeping the Satellite to conserve power - """ - - def __init__(self, cubesat: Satellite, logger: Logger): - """ - Creates a SleepHelper object. - - :param cubesat: The Satellite object - :param logger: The Logger object allowing for log output - - """ - self.cubesat: Satellite = cubesat - self.logger: Logger = logger - - def safe_sleep(self, duration: int = 15) -> None: - """ - Puts the Satellite to sleep for specified duration, in seconds. - - Current implementation results in an actual sleep duration that is a multiple of 15. - Current implementation only allows for a maximum sleep duration of 180 seconds. - - :param duration: Specified time, in seconds, to sleep the Satellite for - """ - - self.logger.info("Setting Safe Sleep Mode") - - iterations: int = 0 - - while duration >= 15 and iterations < 12: - time_alarm: circuitpython_typing.Alarm = alarm.time.TimeAlarm( - monotonic_time=time.monotonic() + 15 - ) - - alarm.light_sleep_until_alarms(time_alarm) - duration -= 15 - iterations += 1 - - self.cubesat.watchdog_pet() - - def short_hibernate(self) -> Literal[True]: - """Puts the Satellite to sleep for 120 seconds""" - - self.logger.debug("Short Hibernation Coming UP") - gc.collect() - # all should be off from cubesat powermode - - # checking the type of self.cubesat.enable_rf, as it can be a DigitalInOut object or a bool. - if isinstance(self.cubesat.enable_rf, digitalio.DigitalInOut): - self.cubesat.enable_rf.value = False - else: - self.cubesat.enable_rf = False - - self.cubesat.f_softboot.toggle(True) - self.cubesat.watchdog_pet() - self.safe_sleep(120) - - # checking the type of self.cubesat.enable_rf, as it can be a DigitalInOut object or a bool. - if isinstance(self.cubesat.enable_rf, digitalio.DigitalInOut): - self.cubesat.enable_rf.value = True - else: - self.cubesat.enable_rf = True - - return True - - def long_hibernate(self) -> Literal[True]: - """Puts the Satellite to sleep for 180 seconds""" - - self.logger.debug("LONG Hibernation Coming UP") - gc.collect() - # all should be off from cubesat powermode - - # checking the type of self.cubesat.enable_rf, as it can be a DigitalInOut object or a bool. - if isinstance(self.cubesat.enable_rf, digitalio.DigitalInOut): - self.cubesat.enable_rf.value = False - else: - self.cubesat.enable_rf = False - - self.cubesat.f_softboot.toggle(True) - self.cubesat.watchdog_pet() - self.safe_sleep(600) - - # checking the type of self.cubesat.enable_rf, as it can be a DigitalInOut object or a bool. - if isinstance(self.cubesat.enable_rf, digitalio.DigitalInOut): - self.cubesat.enable_rf.value = True - else: - self.cubesat.enable_rf = True - - return True From 085da3caa055ee3786153b835ade8172dc6742d7 Mon Sep 17 00:00:00 2001 From: aychar <58487401+hrfarmer@users.noreply.github.com> Date: Wed, 26 Mar 2025 01:55:36 -0500 Subject: [PATCH 6/8] remove pysquared from requirements.txt --- lib/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/requirements.txt b/lib/requirements.txt index d4b0d17..2286cc2 100644 --- a/lib/requirements.txt +++ b/lib/requirements.txt @@ -10,4 +10,3 @@ adafruit-circuitpython-rfm==1.0.3 adafruit-circuitpython-tca9548a @ git+https://github.com/proveskit/Adafruit_CircuitPython_TCA9548A adafruit-circuitpython-ticks==1.1.1 adafruit-circuitpython-veml7700==2.0.2 -pysquared @ git+https://github.com/hrfarmer/pysquared From 40434aaae0a1cd5ec72a93a6c31d59171308f57e Mon Sep 17 00:00:00 2001 From: aychar <58487401+hrfarmer@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:35:38 -0500 Subject: [PATCH 7/8] fix formatting again --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 070a1f5..52099c1 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ download-libraries: .venv ## Download the required libraries else \ $(UV) pip install git+https://github.com/proveskit/pysquared --target lib --no-deps --upgrade --quiet; \ fi - + @rm -rf lib/*.dist-info @rm -rf lib/.lock From 66735c9517eef4f81326657d7fc1ca2c1c7b96e5 Mon Sep 17 00:00:00 2001 From: aychar <58487401+hrfarmer@users.noreply.github.com> Date: Thu, 27 Mar 2025 17:55:23 -0500 Subject: [PATCH 8/8] remove tests and other unrelated folders --- .github/workflows/ci.yaml | 19 - Makefile | 10 - mocks/circuitpython/adafruit_rfm/rfm9x.py | 8 - mocks/circuitpython/adafruit_rfm/rfm9xfsk.py | 9 - .../circuitpython/adafruit_rfm/rfm_common.py | 8 - mocks/circuitpython/busio.py | 20 - mocks/circuitpython/byte_array.py | 24 -- mocks/circuitpython/digitalio.py | 24 -- mocks/circuitpython/microcontroller.py | 8 - mocks/circuitpython/rtc.py | 12 - stubs/circuitpython/byte_array.py | 45 --- tests/repl/LiDARtest.py | 25 -- tests/repl/Yagi.py | 53 --- tests/repl/comms.py | 61 --- tests/repl/detumbletest.py | 18 - tests/repl/echo.py | 99 ----- tests/repl/facetest.py | 47 --- tests/repl/fsk_test.py | 57 --- tests/repl/packet_receiver.py | 381 ------------------ tests/repl/radio_test.py | 283 ------------- .../unit/lib/pysquared/files/config.test.json | 51 --- .../pysquared/hardware/rfm9x/test_factory.py | 206 ---------- .../pysquared/hardware/rfm9x/test_manager.py | 157 -------- .../unit/lib/pysquared/hardware/test_busio.py | 64 --- .../lib/pysquared/hardware/test_digitalio.py | 56 --- tests/unit/lib/pysquared/nvm/test_counter.py | 33 -- tests/unit/lib/pysquared/nvm/test_flag.py | 57 --- .../lib/pysquared/other/other_test_config.py | 322 --------------- tests/unit/lib/pysquared/rtc/test_rp2040.py | 41 -- .../unit/lib/pysquared/rtc/test_rtc_common.py | 23 -- tests/unit/lib/pysquared/test_config.py | 145 ------- tests/unit/lib/pysquared/test_detumble.py | 105 ----- tests/unit/lib/pysquared/test_logger.py | 189 --------- 33 files changed, 2660 deletions(-) delete mode 100644 mocks/circuitpython/adafruit_rfm/rfm9x.py delete mode 100644 mocks/circuitpython/adafruit_rfm/rfm9xfsk.py delete mode 100644 mocks/circuitpython/adafruit_rfm/rfm_common.py delete mode 100644 mocks/circuitpython/busio.py delete mode 100644 mocks/circuitpython/byte_array.py delete mode 100644 mocks/circuitpython/digitalio.py delete mode 100644 mocks/circuitpython/microcontroller.py delete mode 100644 mocks/circuitpython/rtc.py delete mode 100644 stubs/circuitpython/byte_array.py delete mode 100644 tests/repl/LiDARtest.py delete mode 100644 tests/repl/Yagi.py delete mode 100644 tests/repl/comms.py delete mode 100644 tests/repl/detumbletest.py delete mode 100644 tests/repl/echo.py delete mode 100644 tests/repl/facetest.py delete mode 100644 tests/repl/fsk_test.py delete mode 100644 tests/repl/packet_receiver.py delete mode 100755 tests/repl/radio_test.py delete mode 100644 tests/unit/lib/pysquared/files/config.test.json delete mode 100644 tests/unit/lib/pysquared/hardware/rfm9x/test_factory.py delete mode 100644 tests/unit/lib/pysquared/hardware/rfm9x/test_manager.py delete mode 100644 tests/unit/lib/pysquared/hardware/test_busio.py delete mode 100644 tests/unit/lib/pysquared/hardware/test_digitalio.py delete mode 100644 tests/unit/lib/pysquared/nvm/test_counter.py delete mode 100644 tests/unit/lib/pysquared/nvm/test_flag.py delete mode 100644 tests/unit/lib/pysquared/other/other_test_config.py delete mode 100644 tests/unit/lib/pysquared/rtc/test_rp2040.py delete mode 100644 tests/unit/lib/pysquared/rtc/test_rtc_common.py delete mode 100644 tests/unit/lib/pysquared/test_config.py delete mode 100644 tests/unit/lib/pysquared/test_detumble.py delete mode 100644 tests/unit/lib/pysquared/test_logger.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dbfc396..77145f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,25 +14,6 @@ jobs: - name: Lint run: | make fmt - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - # Disabling shallow clones is recommended for improving the relevancy of SonarQube reporting - fetch-depth: 0 - - name: Test - run: | - TEST_SELECT=ALL make test - - name: Archive coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: .coverage-reports/coverage.xml - - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v4.2.1 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} archive: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 52099c1..7d11de7 100644 --- a/Makefile +++ b/Makefile @@ -38,16 +38,6 @@ pre-commit-install: uv fmt: pre-commit-install ## Lint and format files $(UVX) pre-commit run --all-files -.PHONY: test -test: .venv download-libraries ## Run tests -ifeq ($(TEST_SELECT),ALL) - $(UV) run coverage run --rcfile=pyproject.toml -m pytest tests/unit -else - $(UV) run coverage run --rcfile=pyproject.toml -m pytest -m "not slow" tests/unit -endif - @$(UV) run coverage html --rcfile=pyproject.toml > /dev/null - @$(UV) run coverage xml --rcfile=pyproject.toml > /dev/null - BOARD_MOUNT_POINT ?= "" VERSION ?= $(shell git tag --points-at HEAD --sort=-creatordate < /dev/null | head -n 1) diff --git a/mocks/circuitpython/adafruit_rfm/rfm9x.py b/mocks/circuitpython/adafruit_rfm/rfm9x.py deleted file mode 100644 index 19610a5..0000000 --- a/mocks/circuitpython/adafruit_rfm/rfm9x.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Mock for Adafruit RFM9x -https://github.com/adafruit/Adafruit_CircuitPython_RFM/blob/8a55e345501e038996b2aa89e71d4e5e3ddbdebe/adafruit_rfm/rfm9x.py -""" - - -class RFM9x: - def __init__(self, spi, cs, reset, frequency) -> None: ... diff --git a/mocks/circuitpython/adafruit_rfm/rfm9xfsk.py b/mocks/circuitpython/adafruit_rfm/rfm9xfsk.py deleted file mode 100644 index ad1a568..0000000 --- a/mocks/circuitpython/adafruit_rfm/rfm9xfsk.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -Mock for Adafruit RFM9xFSK -https://github.com/adafruit/Adafruit_CircuitPython_RFM/blob/8a55e345501e038996b2aa89e71d4e5e3ddbdebe/adafruit_rfm/rfm9xfsk.py -""" - - -class RFM9xFSK: - def __init__(self, spi, cs, reset, frequency) -> None: - self.modulation_type = None diff --git a/mocks/circuitpython/adafruit_rfm/rfm_common.py b/mocks/circuitpython/adafruit_rfm/rfm_common.py deleted file mode 100644 index 58f7194..0000000 --- a/mocks/circuitpython/adafruit_rfm/rfm_common.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Mock for Adafruit RFM SPI -https://github.com/adafruit/Adafruit_CircuitPython_RFM/blob/8a55e345501e038996b2aa89e71d4e5e3ddbdebe/adafruit_rfm/rfm_common.py -""" - - -class RFMSPI: - pass diff --git a/mocks/circuitpython/busio.py b/mocks/circuitpython/busio.py deleted file mode 100644 index 4f4f091..0000000 --- a/mocks/circuitpython/busio.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Mock for Circuit Python busio -https://docs.circuitpython.org/en/latest/shared-bindings/microcontroller/index.html -""" - -from __future__ import annotations - -from typing import Optional - -import mocks.circuitpython.microcontroller as microcontroller - - -class SPI: - def __init__( - self, - clock: microcontroller.Pin, - MOSI: Optional[microcontroller.Pin] = None, - MISO: Optional[microcontroller.Pin] = None, - half_duplex: bool = False, - ) -> None: ... diff --git a/mocks/circuitpython/byte_array.py b/mocks/circuitpython/byte_array.py deleted file mode 100644 index 2cea6ed..0000000 --- a/mocks/circuitpython/byte_array.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Union - -from circuitpython_typing import ReadableBuffer - -from stubs.circuitpython.byte_array import ByteArray as ByteArrayStub - - -class ByteArray(ByteArrayStub): - """ - ByteArray is a class that mocks the implementaion of the CircuitPython non-volatile memory API. - """ - - def __init__(self, size: int = 1024) -> None: - self.memory = bytearray(size) - - def __getitem__(self, index: Union[slice, int]) -> Union[bytearray, int]: - if isinstance(index, slice): - return bytearray(self.memory[index]) - return int(self.memory[index]) - - def __setitem__( - self, index: Union[slice, int], value: Union[ReadableBuffer, int] - ) -> None: - self.memory[index] = value diff --git a/mocks/circuitpython/digitalio.py b/mocks/circuitpython/digitalio.py deleted file mode 100644 index 76a4d63..0000000 --- a/mocks/circuitpython/digitalio.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Mock for Circuit Python digitalio -https://docs.circuitpython.org/en/latest/shared-bindings/digitalio/index.html -""" - -from __future__ import annotations - -import mocks.circuitpython.microcontroller as microcontroller - - -class DriveMode: - pass - - -class DigitalInOut: - def __init__(self, pin: microcontroller.Pin) -> None: ... - - -class Direction: - pass - - -class Pull: - pass diff --git a/mocks/circuitpython/microcontroller.py b/mocks/circuitpython/microcontroller.py deleted file mode 100644 index 4fc87e3..0000000 --- a/mocks/circuitpython/microcontroller.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Mock for Circuit Python microcontroller -https://docs.circuitpython.org/en/latest/shared-bindings/busio/index.html -""" - - -class Pin: - pass diff --git a/mocks/circuitpython/rtc.py b/mocks/circuitpython/rtc.py deleted file mode 100644 index e9a3413..0000000 --- a/mocks/circuitpython/rtc.py +++ /dev/null @@ -1,12 +0,0 @@ -class RTC: - _instance = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super(RTC, cls).__new__(cls) - cls._instance.datetime = None - return cls._instance - - @classmethod - def destroy(cls): - cls._instance = None diff --git a/stubs/circuitpython/byte_array.py b/stubs/circuitpython/byte_array.py deleted file mode 100644 index 3c9c4e3..0000000 --- a/stubs/circuitpython/byte_array.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Stub for Circuit Python ByteArray class -https://docs.circuitpython.org/en/stable/shared-bindings/nvm/index.html#nvm.ByteArray - -This stub has been contributed to the Adafruit CircuitPython Typing repo and can be removed after it has been approved and merged: -https://github.com/adafruit/Adafruit_CircuitPython_Typing/pull/46 -""" - -from typing import Union, overload - -from circuitpython_typing import ReadableBuffer -from typing_extensions import Protocol - - -class ByteArray(Protocol): - """ - Presents a stretch of non-volatile memory as a bytearray. - - Non-volatile memory is available as a byte array that persists over reloads and power cycles. Each assignment causes an erase and write cycle so its recommended to assign all values to change at once. - """ - - def __bool__(self) -> bool: ... - - def __len__(self) -> int: - """Return the length. This is used by (len)""" - - @overload - def __getitem__(self, index: slice) -> bytearray: ... - - @overload - def __getitem__(self, index: int) -> int: ... - - def __getitem__(self, index: Union[slice, int]) -> Union[bytearray, int]: - """Returns the value at the given index.""" - - @overload - def __setitem__(self, index: slice, value: ReadableBuffer) -> None: ... - - @overload - def __setitem__(self, index: int, value: int) -> None: ... - - def __setitem__( - self, index: Union[slice, int], value: Union[ReadableBuffer, int] - ) -> None: - """Set the value at the given index.""" diff --git a/tests/repl/LiDARtest.py b/tests/repl/LiDARtest.py deleted file mode 100644 index 683299d..0000000 --- a/tests/repl/LiDARtest.py +++ /dev/null @@ -1,25 +0,0 @@ -import time - -import adafruit_vl6180x # LiDAR Distance Sensor for Antenna -import board -import busio -import neopixel # RGB LED - -i2c = busio.I2C(board.SCL1, board.SDA1) -# Initialize LiDAR -try: - LiDAR = adafruit_vl6180x.VL6180X(i2c) - LiDAR.offset = 10 -except Exception as e: - print("[ERROR][LiDAR]" + str(e)) -try: - neopixel = neopixel.NeoPixel( - board.NEOPIXEL, 1, brightness=0.1, pixel_order=neopixel.GRB - ) - neopixel[0] = (0, 0, 255) -except Exception as e: - print("[ERROR][neopixel]" + str(e)) -while True: - print("Distance: ", LiDAR.range) - - time.sleep(1) diff --git a/tests/repl/Yagi.py b/tests/repl/Yagi.py deleted file mode 100644 index 5bbec86..0000000 --- a/tests/repl/Yagi.py +++ /dev/null @@ -1,53 +0,0 @@ -from pysquared import cubesat - - -class Yagi: - def __init__(self): - self.cubesat = cubesat - self.cubesat.radio1.spreading_factor = 8 - self.cubesat.radio1.tx_power = 23 - self.cubesat.radio1.low_datarate_optimize = False - self.cubesat.radio1.node = 0xFA - self.cubesat.radio1.destination = 0xFF - self.cubesat.radio1.receive_timeout = 10 - self.cubesat.radio1.enable_crc = False - if self.cubesat.radio1.spreading_factor > 8: - self.cubesat.radio1.low_datarate_optimize = True - - def yagiSide(self): - print( - "Listening for transmissions, {}s".format( - self.cubesat.radio1.receive_timeout - ) - ) - heard_something = self.cubesat.radio1.await_rx(timeout=10) - - if heard_something: - response = self.cubesat.radio1.receive(keep_listening=True) - - if response is not None: - print("packet received") - print( - "msg: {}, RSSI: {}".format( - response, self.cubesat.radio1.last_rssi - 137 - ) - ) - - # self.cubesat.radio1.send('Received! Echo:{}'.format(self.cubesat.radio1.last_rssi-137)) - print("Echo sent") - else: - print("no packets received") - - # self.cubesat.radio1.send('Nothing Received') - print("Echo sent") - - def yagirun(self, timeout=10, spread=8): - self.cubesat.radio1.receive_timeout = timeout - self.cubesat.radio1.spreading_factor = spread - if self.cubesat.radio1.spreading_factor > 8: - self.cubesat.radio1.low_datarate_optimize = True - while True: - self.yagiSide() - - -test = Yagi() diff --git a/tests/repl/comms.py b/tests/repl/comms.py deleted file mode 100644 index c11b45d..0000000 --- a/tests/repl/comms.py +++ /dev/null @@ -1,61 +0,0 @@ -from pysquared import cubesat - - -class Field: - def __init__(self): - self.testMsg = [] - self.testMsg.append("Hello There!") - self.testMsg.append( - "I have brought peace, freedom, justice, and security to my new empire. Your new empire!?" - ) - self.testMsg.append( - "ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh" - ) - self.cubesat = cubesat - self.cubesat.enable_rf.value = True - self.cubesat.radio1.spreading_factor = 8 - self.cubesat.radio1.low_datarate_optimize = False - self.cubesat.radio1.node = 0xFB - self.cubesat.radio1.destination = 0xFA - self.cubesat.radio1.receive_timeout = 10 - self.cubesat.radio1.enable_crc = True - self.cubesat.all_faces_off() - - def fieldSide(self, msg): - print("Sending test message:") - self.cubesat.radio1.send( - self.testMsg[msg] + " SF: " + str(self.cubesat.radio1.spreading_factor), - keep_listening=True, - ) - - print( - "Listening for transmissions, {}s".format( - self.cubesat.radio1.receive_timeout - ) - ) - heard_something = self.cubesat.radio1.await_rx(timeout=20) - - if heard_something: - response = self.cubesat.radio1.receive(keep_listening=True) - - if response is not None: - print("packet received") - print( - "msg: {}, RSSI: {}".format( - response, self.cubesat.radio1.last_rssi - 137 - ) - ) - - else: - print("no packets received") - - def fieldrun(self, timeout=10, spread=8, msg=0): - self.cubesat.radio1.receive_timeout = timeout - self.cubesat.radio1.spreading_factor = spread - if self.cubesat.radio1.spreading_factor > 8: - self.cubesat.radio1.low_datarate_optimize = True - while True: - self.fieldSide(msg) - - -test = Field() diff --git a/tests/repl/detumbletest.py b/tests/repl/detumbletest.py deleted file mode 100644 index f3105a8..0000000 --- a/tests/repl/detumbletest.py +++ /dev/null @@ -1,18 +0,0 @@ -import time - -import functions -from debugcolor import co -from pysquared import cubesat as c - - -def debug_print(statement): - if c.debug: - print(co("[MAIN]" + statement, "blue", "bold")) - - -f = functions.functions(c) -while True: - f.detumble() - f.send("Detumble finished") - c.RGB = (255, 255, 0) - time.sleep(5) diff --git a/tests/repl/echo.py b/tests/repl/echo.py deleted file mode 100644 index 7b41d50..0000000 --- a/tests/repl/echo.py +++ /dev/null @@ -1,99 +0,0 @@ -import time - -from pycubed import cubesat - -yagiMode = True -# testMsg = "According to all known laws of aviation, there is no way a bee should be able to fly." -# testMsg = "Its over Anakin I have the high ground!" -# testMsg = "I have brought peace, freedom, justice, and security to my new empire. Your new empire!?" -# testMsg = "Hello There!" -testMsg = "ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh" - -cubesat.radio1.spreading_factor = 8 -cubesat.radio1.tx_power = 13 -cubesat.radio1.low_datarate_optimize = False -cubesat.radio1.node = 0xFB -cubesat.radio1.destination = 0xFA -cubesat.radio1.receive_timeout = 15 -cubesat.radio1.enable_crc = False - -if cubesat.radio1.spreading_factor > 8: - cubesat.radio1.low_datarate_optimize = True - - -def yagiSide(): - print("Listening for transmissions, 5s") - heard_something = cubesat.radio1.await_rx(timeout=10) - - if heard_something: - response = cubesat.radio1.receive(keep_listening=True) - - if response is not None: - print("packet received") - print("msg: {}, RSSI: {}".format(response, cubesat.radio1.last_rssi - 137)) - - cubesat.radio1.send("Echo:{}".format(cubesat.radio1.last_rssi - 137)) - print("Echo sent") - time.sleep(1) - - time.sleep(2) - - -def cop_yagiSide(): - print("Listening for transmissions, 5s") - heard_something = cubesat.radio1.await_rx(timeout=10) - - if heard_something: - response = cubesat.radio1.receive(keep_listening=True) - - if response is not None: - print("packet received") - print("msg: {}, RSSI: {}".format(response, cubesat.radio1.last_rssi - 137)) - - cubesat.radio1.send("Echo:{}".format(cubesat.radio1.last_rssi - 137)) - print("Echo sent") - time.sleep(1) - - time.sleep(2) - - -def fieldSide(): - print("Sending test message:") - cubesat.radio1.send( - testMsg + " SF: " + str(cubesat.radio1.spreading_factor), keep_listening=True - ) - - print("Listening for transmissions, 5s") - heard_something = cubesat.radio1.await_rx(timeout=2) - - if heard_something: - response = cubesat.radio1.receive(keep_listening=True) - - if response is not None: - print("packet received") - print("msg: {}, RSSI: {}".format(response, cubesat.radio1.last_rssi - 137)) - - time.sleep(2) - - -def banger(): - print("Banging away") - - cubesat.radio1.send(testMsg + " SF: " + str(cubesat.radio1.spreading_factor)) - - response = cubesat.radio1.receive(keep_listening=True) - - if response is not None: - print("packet received") - print("msg: {}, RSSI: {}".format(response, cubesat.radio1.last_rssi - 137)) - - time.sleep(2) - - -while True: - # banger() - - if yagiMode: - yagiSide() - else: - fieldSide() diff --git a/tests/repl/facetest.py b/tests/repl/facetest.py deleted file mode 100644 index 7203361..0000000 --- a/tests/repl/facetest.py +++ /dev/null @@ -1,47 +0,0 @@ -import time - -import adafruit_drv2605 -import adafruit_mcp9808 -import adafruit_pca9685 -import adafruit_tca9548a -import adafruit_veml7700 -import board -import busio -import ina219 - -i2c = busio.I2C(board.SCL0, board.SDA0) -pca = adafruit_pca9685.PCA9685(i2c, address=86) -tca = adafruit_tca9548a.TCA9548A(i2c, address=119) -pca.frequency = 60 -pca.channels[0].duty_cycle = 0xFFFF -pca.channels[1].duty_cycle = 0xFFFF -veml0 = adafruit_veml7700.VEML7700(tca[0]) -mcp0 = adafruit_mcp9808.MCP9808(tca[0], address=27) -drv0 = adafruit_drv2605.DRV2605(tca[0]) -veml1 = adafruit_veml7700.VEML7700(tca[1]) -mcp1 = adafruit_mcp9808.MCP9808(tca[1], address=27) -try: - drv1 = adafruit_drv2605.DRV2605(tca[1]) -except Exception: - drv1 = adafruit_drv2605.DRV2605(tca[1], address=95) -ina = ina219.INA219(tca[5], addr=64) -drv0.sequence[0] = adafruit_drv2605.Effect(47) -drv1.sequence[0] = adafruit_drv2605.Effect(47) -while True: - drv0.play() - drv1.play() - print("VEML7700 0: ", veml0.lux) - print("MCP9808 0: ", mcp0.temperature) - print("DRV2605 0: ", drv0.sequence[0]) - print("VEML7700 1: ", veml1.lux) - print("MCP9808 1: ", mcp1.temperature) - print("DRV2605 1: ", drv1.sequence[0]) - print( - "INA219: {:6.3f}V, {:7.4}mA, {:8.5}mW".format( - ina.bus_voltage, ina.current, ina.power - ) - ) - time.sleep(1) - drv0.stop() - drv1.stop() - time.sleep(1) diff --git a/tests/repl/fsk_test.py b/tests/repl/fsk_test.py deleted file mode 100644 index c591297..0000000 --- a/tests/repl/fsk_test.py +++ /dev/null @@ -1,57 +0,0 @@ -from pysquared import cubesat - -test_message = "Hello There!" -debug_mode = True -number_of_attempts = 0 - -# Radio Configuration Setup Here -radio_cfg = { - "spreading_factor": 8, - "tx_power": 13, # Set as a default that works for any radio - "node": 0x00, - "destination": 0x00, - "receive_timeout": 5, - "enable_crc": False, -} - -# Setting the Radio -cubesat.radio1.spreading_factor = radio_cfg["spreading_factor"] -if cubesat.radio1.spreading_factor > 8: - cubesat.radio1.low_datarate_optimize = True -else: - cubesat.radio1.low_datarate_optimize = False -cubesat.radio1.tx_power = radio_cfg["tx_power"] -cubesat.radio1.receive_timeout = radio_cfg["receive_timeout"] -cubesat.radio1.enable_crc = False - -cubesat.radio1.send(bytes("Hello world KN6YZZ!\r\n", "utf-8")) -print("Sent Hello World message!") - -# Wait to receive packets. -print("Waiting for packets...") - -while True: - packet = cubesat.radio1.receive() - # Optionally change the receive timeout from its default of 0.5 seconds: - # packet = rfm9x.receive(timeout=5.0) - # If no packet was received during the timeout then None is returned. - if packet is None: - # Packet has not been received - print("Received nothing! Listening again...") - else: - # Received a packet! - # Print out the raw bytes of the packet: - print(f"Received (raw bytes): {packet}") - # And decode to ASCII text and print it too. Note that you always - # receive raw bytes and need to convert to a text format like ASCII - # if you intend to do string processing on your data. Make sure the - # sending side is sending ASCII data before you try to decode! - try: - packet_text = str(packet, "ascii") - print(f"Received (ASCII): {packet_text}") - except UnicodeError: - print("Hex data: ", [hex(x) for x in packet]) - # Also read the RSSI (signal strength) of the last received message and - # print it. - rssi = cubesat.radio1.last_rssi - print(f"Received signal strength: {rssi} dB") diff --git a/tests/repl/packet_receiver.py b/tests/repl/packet_receiver.py deleted file mode 100644 index 32b3fa2..0000000 --- a/tests/repl/packet_receiver.py +++ /dev/null @@ -1,381 +0,0 @@ -class PacketReceiver: - def __init__(self, radio, packet_manager, receive_delay=1.0): - """ - Initialize the packet receiver - - Args: - radio: The radio object for receiving - packet_manager: Instance of PacketManager - receive_delay: Delay between receive attempts (default 1.0 seconds) - """ - self.radio = radio - self.pm = packet_manager - self.receive_delay = receive_delay - self.reset() - - def reset(self): - """Reset the receiver state""" - self.received_packets = {} - self.total_packets = None - self.start_time = None - - def process_packet(self, packet): - """Process a single received packet""" - print(f"\nProcessing packet of length: {len(packet)}") - print(f"Header bytes: {[hex(b) for b in packet[:4]]}") - - if self.pm.is_ack_packet(packet): - print("Packet is an ACK packet, skipping") - return False, None - - try: - seq_num = int.from_bytes(packet[:2], "big") - packet_total = int.from_bytes(packet[2:4], "big") - print(f"Decoded - Sequence: {seq_num}, Total packets: {packet_total}") - - if self.total_packets is None: - self.total_packets = packet_total - print(f"Set total expected packets to: {self.total_packets}") - elif packet_total != self.total_packets: - print( - f"Warning: Packet indicates different total ({packet_total}) than previously recorded ({self.total_packets})" - ) - - # Store packet and send ACK if it's new - if seq_num not in self.received_packets: - self.received_packets[seq_num] = packet - print(f"Stored new packet {seq_num}") - self.send_ack(seq_num) - else: - print(f"Duplicate packet {seq_num}, resending ACK") - self.send_ack(seq_num) - - # Check if we have all packets - if ( - self.total_packets is not None - and len(self.received_packets) == self.total_packets - and all(i in self.received_packets for i in range(self.total_packets)) - ): - print("All packets received!") - return True, seq_num - - missing = self.get_missing_packets() - print(f"Missing packets: {missing}") - return False, seq_num - - except Exception as e: - print(f"Error processing packet: {e}") - import traceback - - traceback.print_exc() - return False, None - - def send_ack(self, seq_num, num_acks=3, ack_delay=0.1): - """ - Send multiple acknowledgments for a packet with delays - - Args: - seq_num: Sequence number to acknowledge - num_acks: Number of ACKs to send - ack_delay: Delay between ACKs - """ - import time - - ack = self.pm.create_ack_packet(seq_num) - - for i in range(num_acks): - print(f"Sending ACK {i+1}/{num_acks} for packet {seq_num}") - self.radio.send(ack, keep_listening=True) - if i < num_acks - 1: # Don't delay after last ACK - time.sleep(ack_delay) - - def get_missing_packets(self): - """Return list of missing packet sequence numbers""" - if self.total_packets is None: - return [] - return [i for i in range(self.total_packets) if i not in self.received_packets] - - def receive_until_complete(self, timeout=30.0): - """ - Receive packets until complete message received or timeout - - Args: - timeout: Total time to wait for complete message - - Returns: - Tuple of (success, data, stats) - """ - import time - - print("\nStarting receiver...") - self.reset() - self.start_time = time.monotonic() - - stats = { - "packets_received": 0, - "duplicate_packets": 0, - "invalid_packets": 0, - "time_elapsed": 0, - "receive_attempts": 0, - } - - while True: - current_time = time.monotonic() - - # Check timeout - if current_time - self.start_time > timeout: - print("\nTimeout reached") - print(f"Final state: {len(self.received_packets)} packets received") - if self.total_packets is not None: - print(f"Missing packets: {self.get_missing_packets()}") - stats["time_elapsed"] = current_time - self.start_time - return False, None, stats - - # Single receive attempt with delay - stats["receive_attempts"] += 1 - packet = self.radio.receive() - print(packet) # This print helps with radio timing/synchronization - - if packet: - print(f"\nReceived packet of length: {len(packet)}") - print(f"Raw packet bytes: {[hex(b) for b in packet[:8]]}") - - current_packet_count = len(self.received_packets) - is_complete, seq_num = self.process_packet(packet) - - # Update statistics - if seq_num is not None: - if len(self.received_packets) > current_packet_count: - stats["packets_received"] += 1 - print( - f"New packet received, total: {stats['packets_received']}" - ) - else: - stats["duplicate_packets"] += 1 - print( - f"Duplicate packet received, total: {stats['duplicate_packets']}" - ) - else: - stats["invalid_packets"] += 1 - print(f"Invalid packet received, total: {stats['invalid_packets']}") - - if is_complete: - print("Reception complete!") - stats["time_elapsed"] = time.monotonic() - self.start_time - return True, self.get_received_data(), stats - - # Delay between attempts for radio synchronization - time.sleep(self.receive_delay) - - # Status update every N attempts - updates_per_minute = 12 # About every 5 seconds with 1-second delay - if stats["receive_attempts"] % (updates_per_minute) == 0: - print( - f"\nWaiting for packets... Time remaining: {round(timeout - (current_time - self.start_time), 1)} seconds" - ) - print(f"Receive attempts: {stats['receive_attempts']}") - if self.total_packets is not None: - print( - f"Have {len(self.received_packets)}/{self.total_packets} packets" - ) - print(f"Missing packets: {self.get_missing_packets()}") - - def send_retransmit_request(self, missing_packets): - """Send request for missing packets with adjusted timing""" - import time - - print(f"\nRequesting retransmission of {len(missing_packets)} packets") - - request = self.pm.create_retransmit_request(missing_packets) - retransmit_timeout = max(10, len(missing_packets) * 1.0) # Longer timeout - - # Send request multiple times with longer gaps - for i in range(2): # Reduced to 2 attempts to avoid flooding - print(f"Sending retransmit request attempt {i+1}/2") - self.radio.send(request, keep_listening=True) - time.sleep(0.2) - - # Wait for retransmitted packets - start_time = time.monotonic() - original_missing = set(missing_packets) - # last_receive_time = start_time - - print("Waiting for retransmitted packets...") - while time.monotonic() - start_time < retransmit_timeout: - packet = self.radio.receive(keep_listening=True) - print(packet) - time.sleep(0.5) - - if packet: - # last_receive_time = time.monotonic() - try: - seq_num = int.from_bytes(packet[:2], "big") - if seq_num in original_missing: - self.received_packets[seq_num] = packet - original_missing.remove(seq_num) - print(f"Successfully received retransmitted packet {seq_num}") - print(f"Still missing: {list(original_missing)}") - - if not original_missing: - print("All requested packets received!") - return True - except Exception as e: - print(f"Error processing retransmitted packet: {e}") - - remaining = list(original_missing) - if remaining: - print(f"Retransmission incomplete. Still missing: {remaining}") - return False - - def fast_receive_until_complete( - self, timeout=30.0, idle_timeout=5, max_retransmit_attempts=3 - ): - """ - Fast receive with automatic retransmission after idle period - - Args: - timeout: Total time to wait for complete message - idle_timeout: Time to wait with no new packets before requesting retransmit - max_retransmit_attempts: Maximum number of retransmit attempts - """ - import time - - print("\nStarting fast receiver...") - self.reset() - self.start_time = time.monotonic() - last_packet_time = time.monotonic() - - stats = { - "packets_received": 0, - "duplicate_packets": 0, - "invalid_packets": 0, - "time_elapsed": 0, - "retransmit_rounds": 0, - } - - # First, wait for and ACK the initial packet - while True: - if time.monotonic() - self.start_time > timeout: - return False, None, stats - - packet = self.radio.receive() - print(packet) - - if packet: - try: - last_packet_time = time.monotonic() - seq_num = int.from_bytes(packet[:2], "big") - self.total_packets = int.from_bytes(packet[2:4], "big") - - if seq_num == 0: # First packet - print( - f"Received first packet. Expecting {self.total_packets} total packets" - ) - self.received_packets[0] = packet - stats["packets_received"] += 1 - self.send_ack(0) # ACK only the first packet - break - except Exception as e: - print(f"Error processing first packet: {e}") - - time.sleep(self.receive_delay) - - # Now receive remaining packets without ACKs - print("Receiving remaining packets...") - receive_end_time = time.monotonic() + timeout - - while time.monotonic() < receive_end_time: - current_time = time.monotonic() - - packet = self.radio.receive() - print(packet) - - if packet: - try: - seq_num = int.from_bytes(packet[:2], "big") - # packet_total = int.from_bytes(packet[2:4], "big") - - if seq_num not in self.received_packets: - self.received_packets[seq_num] = packet - stats["packets_received"] += 1 - print(f"Received packet {seq_num}/{self.total_packets}") - last_packet_time = current_time # Update last packet time - else: - stats["duplicate_packets"] += 1 - - # Check if we have all packets - if len(self.received_packets) == self.total_packets: - if all( - i in self.received_packets - for i in range(self.total_packets) - ): - print("All packets received!") - stats["time_elapsed"] = time.monotonic() - self.start_time - return True, self.get_received_data(), stats - - except Exception as e: - stats["invalid_packets"] += 1 - print(f"Error processing packet: {e}") - - time.sleep(self.receive_delay) - - # Print status every 10 packets - if stats["packets_received"] % 10 == 0: - missing = self.get_missing_packets() - print(f"Have {len(self.received_packets)}/{self.total_packets} packets") - print(f"Missing: {missing}") - # Check if we've been idle too long - if current_time - last_packet_time > idle_timeout: - missing = self.get_missing_packets() - if missing: - print(f"\nNo packets received for {idle_timeout} seconds") - print(f"Missing {len(missing)} packets: {missing}") - - if stats["retransmit_rounds"] < max_retransmit_attempts: - stats["retransmit_rounds"] += 1 - print( - f"Requesting retransmission (attempt {stats['retransmit_rounds']}/{max_retransmit_attempts})" - ) - - if self.send_retransmit_request(missing): - print("Retransmission successful!") - if not self.get_missing_packets(): - return True, self.get_received_data(), stats - else: - print("Retransmission failed") - - # Reset idle timer after retransmit attempt - last_packet_time = current_time - else: - print( - f"Max retransmit attempts ({max_retransmit_attempts}) reached" - ) - break - - # Final retransmit attempt if needed - missing = self.get_missing_packets() - if missing: - print(f"\nTransfer incomplete. Missing {len(missing)} packets") - print(f"Missing packet numbers: {missing}") - stats["time_elapsed"] = time.monotonic() - self.start_time - return False, None, stats - else: - print("\nTransfer complete!") - stats["time_elapsed"] = time.monotonic() - self.start_time - return True, self.get_received_data(), stats - - def get_received_data(self): - """ - Attempt to reassemble and return received data - - Returns: - Reassembled data if complete, None if incomplete - """ - if not self.received_packets or self.total_packets is None: - return None - - if len(self.received_packets) != self.total_packets: - return None - - packets_list = [self.received_packets[i] for i in range(self.total_packets)] - return self.pm.unpack_data(packets_list) diff --git a/tests/repl/radio_test.py b/tests/repl/radio_test.py deleted file mode 100755 index 3c12bd4..0000000 --- a/tests/repl/radio_test.py +++ /dev/null @@ -1,283 +0,0 @@ -# radio_test.py -# V1.0 June 24, 2024 -# Authored by: Michael Pham - -# The is a test script to facilitate a simple ping pong style communications test between two radios. - -import time - -from pysquared import cubesat - -test_message = "Hello There!" -debug_mode = True -number_of_attempts = 0 -cube_callsign = "" - -if cube_callsign == "": - print("No cube callsign!") - exit() - -# Radio Configuration Setup Here -radio_cfg = { - "spreading_factor": 8, - "tx_power": 13, # Set as a default that works for any radio - "node": 0x00, - "destination": 0x00, - "receive_timeout": 5, - "enable_crc": False, -} - -if input("FSK or LoRa? [L/f]") == "F": - cubesat.f_fsk.toggle(True) - del cubesat - print("Resetting in FSK") - from pysquared import cubesat - -print("FSK: " + str(cubesat.f_fsk.get())) - -options = ["A", "B", "C"] - -# Setting the Radio -cubesat.radio1.spreading_factor = radio_cfg["spreading_factor"] -if cubesat.radio1.spreading_factor > 8: - cubesat.radio1.low_datarate_optimize = True -else: - cubesat.radio1.low_datarate_optimize = False -cubesat.radio1.tx_power = radio_cfg["tx_power"] -cubesat.radio1.receive_timeout = radio_cfg["receive_timeout"] -cubesat.radio1.enable_crc = False - -print( - """ -======================================= -| | -| WELCOME! | -| Radio Test Version 1.0 | -| | -======================================= -| Please Select Your Node | -| 'A': Device Under Test | -| 'B': Receiver | -================ OR =================== -| Act as a client | -| 'C': for an active satalite | -======================================= -""" -) - - -def debug_print(message): - if debug_mode: - print(message) - - -def device_under_test(attempts): - debug_print("Device Under Test Selected") - debug_print("Setting up Radio...") - - cubesat.radio1.node = 0xFA - cubesat.radio1.destination = 0xFB - - debug_print("Radio Setup Complete") - debug_print("Sending Ping...") - - print(f"Attempt: {attempts}") - cubesat.radio1.send(test_message) - - debug_print("Ping Sent") - debug_print("Awaiting Response...") - - heard_something = cubesat.radio1.receive(timeout=10) - - if heard_something: - handle_ping() - - else: - debug_print("No Response Received") - - cubesat.radio1.send("Nothing Received") - debug_print("Echo Sent") - - -def receiver(): - debug_print("Receiver Selected") - debug_print("Setting up Radio...") - - cubesat.radio1.node = 0xFA - cubesat.radio1.destination = 0xFB - - debug_print("Radio Setup Complete") - debug_print("Awaiting Ping...") - - heard_something = cubesat.radio1.receive(timeout=10) - - if heard_something: - handle_ping() - - else: - debug_print("No Ping Received") - - cubesat.radio1.send("Nothing Received") - debug_print("Echo Sent") - - -def client(passcode): - debug_print("Client Selected") - debug_print("Setting up radio") - - cubesat.radio1.node = 0xFA - cubesat.radio1.destination = 0xFB - - print( - """ - =============== /\\ =============== - = Please select command :) = - ================================== - 1 - noop | - 2 - hreset | - 3 - shutdown | - 4 - query | - 5 - exec_cmd | - 6 - joke_reply | - 7 - FSK | - 8 - Repeat Code | - ================================== - """ - ) - - chosen_command = input("Select cmd pls: ") - - packet = b"" - - if chosen_command == "1": - packet = b"\x00\x00\x00\x00" + passcode.encode() + b"\x8eb" - elif chosen_command == "2": - packet = b"\x00\x00\x00\x00" + passcode.encode() + b"\xd4\x9f" - elif chosen_command == "3": - packet = ( - b"\x00\x00\x00\x00" + passcode.encode() + b"\x12\x06" + b"\x0b\xfdI\xec" - ) - elif chosen_command == "4": - packet = b"\x00\x00\x00\x00" + passcode.encode() + b"8\x93" + input() - elif chosen_command == "5": - packet = ( - b"\x00\x00\x00\x00" + passcode.encode() + b"\x96\xa2" + input("Command: ") - ) - elif chosen_command == "6": - packet = b"\x00\x00\x00\x00" + passcode.encode() + b"\xa5\xb4" - elif chosen_command == "7": - packet = b"\x00\x00\x00\x00" + passcode.encode() + b"\x56\xc4" - elif chosen_command == "8": - packet = ( - b"\x00\x00\x00\x00" - + passcode.encode() - + b"RP" - + input("Message to Repeat: ") - ) - else: - print( - "Command is not valid or not implemented open radio_test.py and add them yourself!" - ) - - tries = 0 - while True: - msg = cubesat.radio1.receive() - - if msg is not None: - msg_string = "".join([chr(b) for b in msg]) - print(f"Message Received {msg_string}") - print(msg_string[:6]) - - if msg_string[:6] == cube_callsign: - time.sleep(0.1) - tries += 1 - if tries > 5: - print("We tried 5 times! And there was no response. Quitting.") - break - success = cubesat.radio1.send_with_ack(packet) - print("Success " + str(success)) - if success is True: - response = cubesat.radio1.receive(keep_listening=True) - time.sleep(0.5) - - if response is not None: - print( - "msg: {}, RSSI: {}".format( - response, cubesat.radio1.last_rssi - 137 - ) - ) - break - else: - debug_print("No response, trying again (" + str(tries) + ")") - - -def handle_ping(): - response = cubesat.radio1.receive(keep_listening=True) - - if response is not None: - debug_print("Ping Received") - print("msg: {}, RSSI: {}".format(response, cubesat.radio1.last_rssi - 137)) - - cubesat.radio1.send( - "Ping Received! Echo:{}".format(cubesat.radio1.last_rssi - 137) - ) - debug_print("Echo Sent") - else: - debug_print("No Ping Received") - - cubesat.radio1.send("Nothing Received") - debug_print("Echo Sent") - - -device_selection = input() - -if device_selection not in options: - print("Invalid Selection.") - print("Please refresh the device and try again.") - -else: - print( - """ - ======================================= - | | - | Verbose Output? (Y/N) | - | | - ======================================= - """ - ) - - verbose_selection = input() - - if verbose_selection == "Y": - debug_mode = True - elif verbose_selection == "N": - debug_mode = False - -print( - """ -======================================= -| | -| Beginning Radio Test | -| Radio Test Version 1.0 | -| | -======================================= -""" -) - -passcode = "" -if device_selection == "C": - passcode = input( - "What's the passcode (in plain text, will automagically be converted to UTF-8): " - ) - -while True: - if device_selection == "A": - time.sleep(1) - device_under_test(number_of_attempts) - number_of_attempts += 1 - elif device_selection == "B": - time.sleep(1) - receiver() - elif device_selection == "C": - client(passcode) - time.sleep(1) diff --git a/tests/unit/lib/pysquared/files/config.test.json b/tests/unit/lib/pysquared/files/config.test.json deleted file mode 100644 index d7ae26f..0000000 --- a/tests/unit/lib/pysquared/files/config.test.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "cubesat_name": "Orpheus", - "callsign": "KO6AZM", - "last_battery_temp": 20.0, - "sleep_duration": 30, - "detumble_enable_z": true, - "detumble_enable_x": true, - "detumble_enable_y": true, - "jokes": [ - "Hey it is pretty cold up here, did someone forget to pay the electric bill?" - ], - "debug": true, - "legacy": false, - "heating": false, - "orpheus": true, - "is_licensed": false, - "normal_temp": 20, - "normal_battery_temp": 1, - "normal_micro_temp": 20, - "normal_charge_current": 0.5, - "normal_battery_voltage": 6.9, - "critical_battery_voltage": 6.6, - "battery_voltage": 3.3, - "current_draw": 240.5, - "reboot_time": 3600, - "turbo_clock": false, - "radio": { - "sender_id": 251, - "receiver_id": 250, - "transmit_frequency": 437.4, - "start_time": 80000, - "fsk": { - "broadcast_address": 255, - "node_address": 1, - "modulation_type": 0 - }, - "lora": { - "ack_delay": 0.2, - "coding_rate": 8, - "cyclic_redundancy_check": true, - "max_output": true, - "spreading_factor": 8, - "transmit_power": 23 - } - }, - "super_secret_code": "ABCD", - "repeat_code": "RP", - "joke_reply": [ - "Your Mom" - ] -} diff --git a/tests/unit/lib/pysquared/hardware/rfm9x/test_factory.py b/tests/unit/lib/pysquared/hardware/rfm9x/test_factory.py deleted file mode 100644 index aeb3fe1..0000000 --- a/tests/unit/lib/pysquared/hardware/rfm9x/test_factory.py +++ /dev/null @@ -1,206 +0,0 @@ -import math -from unittest.mock import MagicMock, patch - -import pytest - -from lib.pysquared.config.radio import FSKConfig, LORAConfig, RadioConfig -from lib.pysquared.hardware.exception import HardwareInitializationError -from lib.pysquared.hardware.rfm9x.factory import RFM9xFactory -from lib.pysquared.hardware.rfm9x.modulation import RFM9xModulation -from mocks.circuitpython.adafruit_rfm.rfm9x import RFM9x -from mocks.circuitpython.adafruit_rfm.rfm9xfsk import RFM9xFSK - - -@pytest.fixture -def mock_spi(): - return MagicMock() - - -@pytest.fixture -def mock_chip_select(): - return MagicMock() - - -@pytest.fixture -def mock_reset(): - return MagicMock() - - -@pytest.fixture -def mock_logger(): - return MagicMock() - - -@pytest.fixture -def mock_fsk_config(): - return FSKConfig( - {"broadcast_address": 255, "node_address": 1, "modulation_type": 0} - ) - - -@pytest.fixture -def mock_lora_config(): - return LORAConfig( - { - "ack_delay": 0.2, - "coding_rate": 5, - "cyclic_redundancy_check": True, - "max_output": True, - "spreading_factor": 7, - "transmit_power": 23, - } - ) - - -@pytest.fixture -def mock_radio_config(): - return RadioConfig( - { - "sender_id": 1, - "receiver_id": 2, - "transmit_frequency": 915, - "start_time": 0, - "fsk": {"broadcast_address": 255, "node_address": 1, "modulation_type": 0}, - "lora": { - "ack_delay": 0.2, - "coding_rate": 5, - "cyclic_redundancy_check": True, - "max_output": True, - "spreading_factor": 7, - "transmit_power": 23, - }, - } - ) - - -def test_create_fsk_radio(mock_spi, mock_chip_select, mock_reset, mock_fsk_config): - frequency = 915 - radio = RFM9xFactory.create_fsk_radio( - mock_spi, mock_chip_select, mock_reset, frequency, mock_fsk_config - ) - assert isinstance(radio, RFM9xFSK) - assert radio.fsk_broadcast_address == 255 - assert radio.fsk_node_address == 1 - assert radio.modulation_type == 0 - - -def test_create_lora_radio(mock_spi, mock_chip_select, mock_reset, mock_lora_config): - frequency = 915 - radio = RFM9xFactory.create_lora_radio( - mock_spi, - mock_chip_select, - mock_reset, - frequency, - mock_lora_config, - ) - assert isinstance(radio, RFM9x) - assert math.isclose(radio.ack_delay, 0.2) - assert radio.enable_crc - assert radio.max_output - assert radio.spreading_factor == 7 - assert radio.tx_power == 23 - - -def test_create_lora_radio_high_sf(mock_spi, mock_chip_select, mock_reset): - frequency = 915 - high_sf_config = LORAConfig( - { - "ack_delay": 0.2, - "coding_rate": 5, - "cyclic_redundancy_check": True, - "max_output": True, - "spreading_factor": 10, - "transmit_power": 23, - } - ) - - radio = RFM9xFactory.create_lora_radio( - mock_spi, - mock_chip_select, - mock_reset, - frequency, - high_sf_config, - ) - assert isinstance(radio, RFM9x) - assert radio.preamble_length == 10 - assert radio.low_datarate_optimize == 1 - - -def test_create_fsk( - mock_spi, mock_chip_select, mock_reset, mock_logger, mock_radio_config -): - factory = RFM9xFactory(mock_spi, mock_chip_select, mock_reset, mock_radio_config) - - radio = factory.create( - mock_logger, - RFM9xModulation.FSK, - ) - assert isinstance(radio, RFM9xFSK) - assert radio.node == mock_radio_config.sender_id - assert radio.destination == mock_radio_config.receiver_id - - -def test_create_lora( - mock_spi, mock_chip_select, mock_reset, mock_logger, mock_radio_config -): - factory = RFM9xFactory(mock_spi, mock_chip_select, mock_reset, mock_radio_config) - - radio = factory.create( - mock_logger, - RFM9xModulation.LORA, - ) - assert isinstance(radio, RFM9x) - assert radio.node == mock_radio_config.sender_id - assert radio.destination == mock_radio_config.receiver_id - - -def test_get_instance_modulation(): - mock_fsk_radio = RFM9xFSK(None, None, None, 915) - mock_lora_radio = RFM9x(None, None, None, 915) - - assert RFM9xFactory.get_instance_modulation(mock_fsk_radio) == RFM9xModulation.FSK - assert RFM9xFactory.get_instance_modulation(mock_lora_radio) == RFM9xModulation.LORA - - -@pytest.mark.slow -@patch("lib.pysquared.hardware.rfm9x.factory.RFM9xFactory.create_fsk_radio") -def test_create_with_retries_fsk( - mock_create_fsk_radio, - mock_spi, - mock_chip_select, - mock_reset, - mock_logger, - mock_radio_config, -): - mock_create_fsk_radio.side_effect = Exception("Simulated FSK failure") - - factory = RFM9xFactory(mock_spi, mock_chip_select, mock_reset, mock_radio_config) - - with pytest.raises(HardwareInitializationError): - factory.create( - mock_logger, - RFM9xModulation.FSK, - ) - assert mock_create_fsk_radio.call_count == 3 - - -@pytest.mark.slow -@patch("lib.pysquared.hardware.rfm9x.factory.RFM9xFactory.create_lora_radio") -def test_create_with_retries_lora( - mock_create_lora_radio, - mock_spi, - mock_chip_select, - mock_reset, - mock_logger, - mock_radio_config, -): - mock_create_lora_radio.side_effect = Exception("Simulated LoRa failure") - - factory = RFM9xFactory(mock_spi, mock_chip_select, mock_reset, mock_radio_config) - - with pytest.raises(HardwareInitializationError): - factory.create( - mock_logger, - RFM9xModulation.LORA, - ) - assert mock_create_lora_radio.call_count == 3 diff --git a/tests/unit/lib/pysquared/hardware/rfm9x/test_manager.py b/tests/unit/lib/pysquared/hardware/rfm9x/test_manager.py deleted file mode 100644 index be8b6f8..0000000 --- a/tests/unit/lib/pysquared/hardware/rfm9x/test_manager.py +++ /dev/null @@ -1,157 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from lib.pysquared.hardware.rfm9x.factory import RFM9xFactory -from lib.pysquared.hardware.rfm9x.manager import RFM9xManager -from lib.pysquared.hardware.rfm9x.modulation import RFM9xModulation -from lib.pysquared.logger import Logger -from lib.pysquared.nvm.counter import Counter -from lib.pysquared.nvm.flag import Flag -from mocks.circuitpython.adafruit_rfm.rfm_common import RFMSPI -from mocks.circuitpython.byte_array import ByteArray - - -@pytest.fixture -def mock_logger(): - return Logger(Counter(0, ByteArray(size=8))) - - -@pytest.fixture -def mock_use_fsk(): - return Flag(0, 0, ByteArray(size=8)) - - -@pytest.fixture -def mock_radio_factory(): - return MagicMock(spec=RFM9xFactory) - - -@pytest.mark.parametrize( - "modulation, use_fsk_initial", - [(RFM9xModulation.LORA, False), (RFM9xModulation.FSK, True)], -) -def test_radio_property_creates_radio( - mock_logger: Logger, - mock_radio_factory: MagicMock, - modulation: RFM9xModulation, - use_fsk_initial: bool, -): - mock_radio = MagicMock(spec=RFMSPI) - mock_radio_factory.create.return_value = mock_radio - - # Set the flag to use FSK if required - use_fsk = Flag(0, 0, ByteArray(size=8)) - if use_fsk_initial: - use_fsk.toggle(True) - - manager = RFM9xManager( - mock_logger, - use_fsk, - mock_radio_factory, - is_licensed=True, - ) - - radio = manager.radio - - mock_radio_factory.create.assert_called_once_with( - manager._log, - modulation, - ) - assert radio == mock_radio - assert manager._radio == mock_radio - - # Ensure the next restart is still set to LoRa - assert manager._use_fsk.get() is False - - -def test_set_modulation( - mock_logger: Logger, mock_use_fsk: Flag, mock_radio_factory: MagicMock -): - mock_radio = MagicMock(spec=RFMSPI) - mock_radio_factory.create.return_value = mock_radio - - mock_radio.read_u8 = MagicMock() - manager = RFM9xManager( - mock_logger, mock_use_fsk, mock_radio_factory, is_licensed=True - ) - - manager.set_modulation(RFM9xModulation.LORA) - assert manager._use_fsk.get() is False - - manager.set_modulation(RFM9xModulation.FSK) - assert manager._use_fsk.get() is True - - mock_radio_factory.get_instance_modulation.return_value = RFM9xModulation.FSK - manager.set_modulation(RFM9xModulation.FSK) - assert manager._use_fsk.get() is True - - -@pytest.mark.parametrize( - "raw_value, expected_temperature", - [ - (0b00110010, 193), # Example raw value (50) - (0b10110010, 93), # Example raw value (178) - ], -) -def test_get_temperature( - mock_logger: Logger, - mock_use_fsk: Flag, - mock_radio_factory: MagicMock, - raw_value: int, - expected_temperature: int, -): - mock_radio = MagicMock(spec=RFMSPI) - mock_radio.read_u8 = MagicMock() - mock_radio.read_u8.return_value = raw_value - mock_radio_factory.create.return_value = mock_radio - - manager = RFM9xManager( - mock_logger, mock_use_fsk, mock_radio_factory, is_licensed=True - ) - - actual_temperature = manager.get_temperature() - assert actual_temperature == expected_temperature - mock_radio.read_u8.assert_called_once_with(0x5B) - - -def test_beacon_radio_message( - mock_logger: Logger, - mock_use_fsk: Flag, - mock_radio_factory: MagicMock, - capsys, -): - mock_radio = MagicMock(spec=RFMSPI) - mock_radio.send = MagicMock(return_value=True) - mock_radio_factory.create.return_value = mock_radio - - message = "Testing beaconing function in radio manager." - manager = RFM9xManager( - mock_logger, mock_use_fsk, mock_radio_factory, is_licensed=True - ) - - manager.beacon_radio_message(message) - - mock_radio.send.assert_called_once_with(bytes(message, "UTF-8")) - assert "I am beaconing" in capsys.readouterr().out - - -def test_beacon_radio_message_unlicensed( - mock_logger: Logger, - mock_use_fsk: Flag, - mock_radio_factory: MagicMock, - capsys, -): - mock_radio = MagicMock(spec=RFMSPI) - mock_radio.send = MagicMock(return_value=True) - mock_radio_factory.create.return_value = mock_radio - - message = "Testing beaconing function in radio manager." - manager = RFM9xManager( - mock_logger, mock_use_fsk, mock_radio_factory, is_licensed=False - ) - - manager.beacon_radio_message(message) - - mock_radio.send.assert_not_called() - assert "Radio is not licensed" in capsys.readouterr().out diff --git a/tests/unit/lib/pysquared/hardware/test_busio.py b/tests/unit/lib/pysquared/hardware/test_busio.py deleted file mode 100644 index b70a7e8..0000000 --- a/tests/unit/lib/pysquared/hardware/test_busio.py +++ /dev/null @@ -1,64 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest -from microcontroller import Pin - -from lib.pysquared.hardware.busio import initialize_spi_bus -from lib.pysquared.hardware.exception import HardwareInitializationError -from lib.pysquared.logger import Logger - - -@patch("lib.pysquared.hardware.busio.SPI") -def test_initialize_spi_bus_success(mock_spi: MagicMock): - # Mock the logger - mock_logger = MagicMock(spec=Logger) - - # Mock pins - mock_clock = MagicMock(spec=Pin) - mock_mosi = MagicMock(spec=Pin) - mock_miso = MagicMock(spec=Pin) - - # Mock SPI instance - mock_spi_instance = mock_spi.return_value - - # Test parameters - baudrate = 200000 - phase = 1 - polarity = 1 - bits = 16 - - # Call fn under test - result = initialize_spi_bus( - mock_logger, mock_clock, mock_mosi, mock_miso, baudrate, phase, polarity, bits - ) - - # Assertions - mock_spi.assert_called_once_with(mock_clock, mock_mosi, mock_miso) - mock_spi_instance.try_lock.assert_called_once() - mock_spi_instance.configure.assert_called_once_with(baudrate, phase, polarity, bits) - mock_spi_instance.unlock.assert_called_once() - mock_logger.debug.assert_called_once() - assert result == mock_spi_instance - - -@pytest.mark.slow -@patch("lib.pysquared.hardware.busio.SPI") -def test_initialize_spi_bus_failure(mock_spi: MagicMock): - # Mock the logger - mock_logger = MagicMock(spec=Logger) - - # Mock pins - mock_clock = MagicMock(spec=Pin) - mock_mosi = MagicMock(spec=Pin) - mock_miso = MagicMock(spec=Pin) - - # Mock SPI to raise an exception - mock_spi.side_effect = Exception("Simulated failure") - - # Call the function and assert exception - with pytest.raises(HardwareInitializationError): - initialize_spi_bus(mock_logger, mock_clock, mock_mosi, mock_miso) - - # Assertions - assert mock_spi.call_count == 3 # Called 3 times due to retries - mock_logger.debug.assert_called() diff --git a/tests/unit/lib/pysquared/hardware/test_digitalio.py b/tests/unit/lib/pysquared/hardware/test_digitalio.py deleted file mode 100644 index 7cbe863..0000000 --- a/tests/unit/lib/pysquared/hardware/test_digitalio.py +++ /dev/null @@ -1,56 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest -from digitalio import Direction - -from lib.pysquared.hardware.digitalio import initialize_pin -from lib.pysquared.hardware.exception import HardwareInitializationError -from lib.pysquared.logger import Logger - - -@patch("lib.pysquared.hardware.digitalio.DigitalInOut") -@patch("lib.pysquared.hardware.digitalio.Pin") -def test_initialize_pin_success(mock_pin: MagicMock, mock_digital_in_out: MagicMock): - # Mock the logger - mock_logger = MagicMock(spec=Logger) - - # Mock pin and direction - mock_pin = mock_pin() - mock_direction = Direction.OUTPUT - initial_value = True - - # Mock DigitalInOut instance - mock_dio = mock_digital_in_out.return_value - - # Call fn under test - _ = initialize_pin(mock_logger, mock_pin, mock_direction, initial_value) - - # Assertions - mock_digital_in_out.assert_called_once_with(mock_pin) - assert mock_dio.direction == mock_direction - assert mock_dio.value == initial_value - mock_logger.debug.assert_called_once() - - -@pytest.mark.slow -@patch("lib.pysquared.hardware.digitalio.DigitalInOut") -@patch("lib.pysquared.hardware.digitalio.Pin") -def test_initialize_pin_failure(mock_pin: MagicMock, mock_digital_in_out: MagicMock): - # Mock the logger - mock_logger = MagicMock(spec=Logger) - - # Mock pin and direction - mock_pin = mock_pin() - mock_direction = Direction.OUTPUT - initial_value = True - - # Mock DigitalInOut to raise an exception - mock_digital_in_out.side_effect = Exception("Simulated failure") - - # Call the function and assert exception - with pytest.raises(HardwareInitializationError): - initialize_pin(mock_logger, mock_pin, mock_direction, initial_value) - - # Assertions - assert mock_digital_in_out.call_count == 3 # Called 3 times due to retries - mock_logger.debug.assert_called() diff --git a/tests/unit/lib/pysquared/nvm/test_counter.py b/tests/unit/lib/pysquared/nvm/test_counter.py deleted file mode 100644 index ed306e3..0000000 --- a/tests/unit/lib/pysquared/nvm/test_counter.py +++ /dev/null @@ -1,33 +0,0 @@ -import lib.pysquared.nvm.counter as counter -from mocks.circuitpython.byte_array import ByteArray - - -def test_counter_bounds(): - """ - Test that the counter class correctly handles values that are inside and outside the bounds of its bit length - """ - datastore = ByteArray(size=1) - - index = 0 - count = counter.Counter(index, datastore) - assert count.get() == 0 - - count.increment() - assert count.get() == 1 - - datastore[index] = 255 - assert count.get() == 255 - - count.increment() - assert count.get() == 0 - - -def test_writing_to_multiple_counters_in_same_datastore(): - datastore = ByteArray(size=2) - - count_1 = counter.Counter(0, datastore) - count_2 = counter.Counter(1, datastore) - - count_2.increment() - assert count_1.get() == 0 - assert count_2.get() == 1 diff --git a/tests/unit/lib/pysquared/nvm/test_flag.py b/tests/unit/lib/pysquared/nvm/test_flag.py deleted file mode 100644 index 8f149c9..0000000 --- a/tests/unit/lib/pysquared/nvm/test_flag.py +++ /dev/null @@ -1,57 +0,0 @@ -import pytest - -from lib.pysquared.nvm.flag import Flag -from mocks.circuitpython.byte_array import ByteArray - - -@pytest.fixture -def setup_datastore(): - return ByteArray(size=17) - - -def test_init(setup_datastore): - flag = Flag(16, 0, setup_datastore) # Example flag for softboot - assert flag._index == 16 # Check if _index (index of byte array) is set to 16 - assert flag._bit == 0 # Check if _bit (bit position) is set to first index of byte - assert flag._bit_mask == 0b00000001 # Check if _bit_mask is set correctly - - -def test_get(setup_datastore): - flag = Flag(16, 1, setup_datastore) # Example flag for solar - assert setup_datastore[16] == 0b00000000 - assert not flag.get() # Bit should be 0 by default - - setup_datastore[16] = 0b00000010 # Manually set bit to test - assert flag.get() # Should return true since bit position 1 = 1 - - -def test_toggle(setup_datastore): - flag = Flag(16, 2, setup_datastore) # Example flag for burnarm - assert setup_datastore[16] == 0b00000000 - flag.toggle(False) # Set flag to off (bit to 0) - assert setup_datastore[16] == 0b00000000 - assert not flag.get() # Bit should remain 0 due to 0 by default - - flag.toggle(True) # Set flag to on (bit to 1) - assert setup_datastore[16] == 0b00000100 # Check if bit position 2 = 1 - assert flag.get() # Bit should be flipped to 1 - - flag.toggle(True) # Set flag to on (bit to 1) - assert setup_datastore[16] == 0b00000100 # Check if bit position 2 = 1 - assert flag.get() # Bit should remain 1 due to already being set to on - - flag.toggle(False) # Set flag back to off (bit to 0) - assert setup_datastore[16] == 0b00000000 # Check if bit position 2 = 0 - assert not flag.get() # Bit should be 0 - - -def test_edge_cases(setup_datastore): - first_bit = Flag(0, 0, setup_datastore) - first_bit.toggle(True) - assert setup_datastore[0] == 0b00000001 - assert first_bit.get() - - last_bit = Flag(0, 7, setup_datastore) - last_bit.toggle(True) - assert setup_datastore[0] == 0b10000001 - assert last_bit.get() diff --git a/tests/unit/lib/pysquared/other/other_test_config.py b/tests/unit/lib/pysquared/other/other_test_config.py deleted file mode 100644 index 80d7a29..0000000 --- a/tests/unit/lib/pysquared/other/other_test_config.py +++ /dev/null @@ -1,322 +0,0 @@ -import json -from pathlib import Path -from typing import Any, Dict - -import pytest - -# Schema definition using type hints for documentation -CONFIG_SCHEMA = { - "cubesat_name": str, - "callsign": str, - "last_battery_temp": float, - "sleep_duration": int, - "detumble_enable_z": bool, - "detumble_enable_x": bool, - "detumble_enable_y": bool, - "jokes": list, - "debug": bool, - "legacy": bool, - "heating": bool, - "orpheus": bool, - "is_licensed": bool, - "normal_temp": int, - "normal_battery_temp": int, - "normal_micro_temp": int, - "normal_charge_current": float, - "normal_battery_voltage": float, - "critical_battery_voltage": float, - "battery_voltage": float, - "current_draw": float, - "reboot_time": int, - "turbo_clock": bool, - "radio": dict, - "super_secret_code": str, - "repeat_code": str, - "joke_reply": list, -} - - -def validate_config(config: Dict[str, Any]) -> None: - """Validate config data against schema and business rules.""" - # Validate field presence and types - for field, expected_type in CONFIG_SCHEMA.items(): - if field not in config: - raise ValueError(f"Required field '{field}' is missing") - - value = config[field] - if isinstance(expected_type, list): - if not isinstance(value, list): - raise TypeError(f"Field '{field}' must be a list") - if not value: # Check if list is empty - raise ValueError(f"Field '{field}' cannot be empty") - if not all(isinstance(item, str) for item in value): - raise TypeError(f"All items in '{field}' must be strings") - elif not isinstance(value, expected_type): - raise TypeError(f"Field '{field}' must be of type {expected_type.__name__}") - - # Validate callsign - if not config["callsign"]: - raise ValueError("Callsign cannot be empty") - - # Validate voltage ranges - voltage_fields = [ - "battery_voltage", - "normal_battery_voltage", - "critical_battery_voltage", - ] - for field in voltage_fields: - value = config[field] - if not 0 <= value <= 12.0: - raise ValueError(f"{field} must be between 0V and 12V") - - # Validate current draw - if config["current_draw"] < 0: - raise ValueError("Current draw cannot be negative") - - # Validate time values - time_fields = ["sleep_duration", "reboot_time"] - for field in time_fields: - if config[field] <= 0: - raise ValueError(f"{field} must be positive") - - # Validate radio configuration - if not isinstance(config["radio"], dict): - raise TypeError("radio must be a dictionary") - - # Validate basic radio fields - radio_basic_fields = { - "sender_id": int, - "receiver_id": int, - "transmit_frequency": float, - "start_time": int, - } - - for field, expected_type in radio_basic_fields.items(): - if field not in config["radio"]: - raise ValueError(f"Required radio field '{field}' is missing") - if not isinstance(config["radio"][field], expected_type): - raise TypeError( - f"Radio field '{field}' must be of type {expected_type.__name__}" - ) - - # Validate FSK config - if "fsk" not in config["radio"]: - raise ValueError("Required radio field 'fsk' is missing") - if not isinstance(config["radio"]["fsk"], dict): - raise TypeError("radio.fsk must be a dictionary") - - fsk_fields = { - "broadcast_address": int, - "node_address": int, - "modulation_type": int, - } - - for field, expected_type in fsk_fields.items(): - if field not in config["radio"]["fsk"]: - raise ValueError(f"Required radio.fsk field '{field}' is missing") - if not isinstance(config["radio"]["fsk"][field], expected_type): - raise TypeError( - f"Radio.fsk field '{field}' must be of type {expected_type.__name__}" - ) - - # Validate LoRa config - if "lora" not in config["radio"]: - raise ValueError("Required radio field 'lora' is missing") - if not isinstance(config["radio"]["lora"], dict): - raise TypeError("radio.lora must be a dictionary") - - lora_fields = { - "ack_delay": float, - "coding_rate": int, - "cyclic_redundancy_check": bool, - "max_output": bool, - "spreading_factor": int, - "transmit_power": int, - } - - for field, expected_type in lora_fields.items(): - if field not in config["radio"]["lora"]: - raise ValueError(f"Required radio.lora field '{field}' is missing") - if not isinstance(config["radio"]["lora"][field], expected_type): - raise TypeError( - f"Radio.lora field '{field}' must be of type {expected_type.__name__}" - ) - - # Validate radio config ranges - if not 0 <= config["radio"]["lora"]["transmit_power"] <= 23: - raise ValueError("lora.transmit_power must be between 0 and 23") - if not 400 <= config["radio"]["transmit_frequency"] <= 450: - raise ValueError("transmit_frequency must be between 400 and 450 MHz") - - -def load_config(config_path: str) -> dict: - """Load and parse the config file.""" - try: - with open(config_path, "r") as f: - return json.load(f) - except json.JSONDecodeError as e: - pytest.fail(f"Invalid JSON in config file: {e}") - except FileNotFoundError: - pytest.fail(f"Config file not found at {config_path}") - - -@pytest.fixture -def config_data(): - """Fixture to load the config data.""" - workspace_root = Path(__file__).parent.parent.parent - config_path = workspace_root / "config.json" - return load_config(str(config_path)) - - -def test_config_file_exists(): - """Test that config.json exists.""" - workspace_root = Path(__file__).parent.parent.parent - config_path = workspace_root / "config.json" - assert config_path.exists(), "config.json file not found" - - -def test_config_is_valid_json(config_data): - """Test that config.json is valid JSON.""" - assert isinstance(config_data, dict), "Config file is not a valid JSON object" - - -def test_config_validation(config_data): - """Test that config.json matches the expected schema and business rules.""" - try: - validate_config(config_data) - except (ValueError, TypeError) as e: - pytest.fail(str(e)) - - -def test_field_types(config_data): - """Test that all fields have correct types.""" - # Test string fields - string_fields = ["cubesat_name", "callsign", "super_secret_code", "repeat_code"] - for field in string_fields: - assert isinstance(config_data[field], str), f"{field} must be a string" - - # Test numeric fields - float_fields = [ - "last_battery_temp", - "normal_charge_current", - "normal_battery_voltage", - "critical_battery_voltage", - "current_draw", - "battery_voltage", - ] - for field in float_fields: - assert isinstance(config_data[field], (int, float)), f"{field} must be a number" - - int_fields = [ - "sleep_duration", - "normal_temp", - "normal_battery_temp", - "normal_micro_temp", - "reboot_time", - ] - for field in int_fields: - assert isinstance(config_data[field], int), f"{field} must be an integer" - - # Test boolean fields - bool_fields = [ - "detumble_enable_z", - "detumble_enable_x", - "detumble_enable_y", - "debug", - "legacy", - "heating", - "orpheus", - "is_licensed", - "turbo_clock", - ] - for field in bool_fields: - assert isinstance(config_data[field], bool), f"{field} must be a boolean" - - # Test list fields - list_fields = ["jokes", "joke_reply"] - for field in list_fields: - assert isinstance(config_data[field], list), f"{field} must be a list" - assert all( - isinstance(item, str) for item in config_data[field] - ), f"All items in {field} must be strings" - - # Test radio config - assert isinstance(config_data["radio"], dict), "radio must be a dictionary" - - # Test basic radio fields - radio_basic_fields = { - "sender_id": int, - "receiver_id": int, - "transmit_frequency": float, - "start_time": int, - } - for field, expected_type in radio_basic_fields.items(): - assert isinstance( - config_data["radio"][field], expected_type - ), f"radio.{field} must be a {expected_type.__name__}" - - # Test FSK fields - assert isinstance( - config_data["radio"]["fsk"], dict - ), "radio.fsk must be a dictionary" - fsk_fields = { - "broadcast_address": int, - "node_address": int, - "modulation_type": int, - } - for field, expected_type in fsk_fields.items(): - assert isinstance( - config_data["radio"]["fsk"][field], expected_type - ), f"radio.fsk.{field} must be a {expected_type.__name__}" - - # Test LoRa fields - assert isinstance( - config_data["radio"]["lora"], dict - ), "radio.lora must be a dictionary" - lora_fields = { - "ack_delay": float, - "coding_rate": int, - "cyclic_redundancy_check": bool, - "max_output": bool, - "spreading_factor": int, - "transmit_power": int, - } - for field, expected_type in lora_fields.items(): - assert isinstance( - config_data["radio"]["lora"][field], expected_type - ), f"radio.lora.{field} must be a {expected_type.__name__}" - - -def test_voltage_ranges(config_data): - """Test that voltage values are within expected ranges.""" - voltage_fields = [ - "battery_voltage", - "normal_battery_voltage", - "critical_battery_voltage", - ] - for field in voltage_fields: - value = config_data[field] - assert 5.2 <= value <= 8.4, f"{field} must be between 5.2V and 8.4V" - - -def test_time_values(config_data): - """Test that time values are positive.""" - assert config_data["sleep_duration"] > 0, "sleep_duration must be positive" - assert config_data["reboot_time"] > 0, "reboot_time must be positive" - - -def test_current_draw_positive(config_data): - """Test that current draw is not negative.""" - assert config_data["current_draw"] >= 0, "current_draw cannot be negative" - - -def test_lists_not_empty(config_data): - """Test that list fields are not empty.""" - assert len(config_data["jokes"]) > 0, "jokes list cannot be empty" - assert len(config_data["joke_reply"]) > 0, "joke_reply list cannot be empty" - assert all( - isinstance(joke, str) for joke in config_data["jokes"] - ), "All jokes must be strings" - assert all( - isinstance(reply, str) for reply in config_data["joke_reply"] - ), "All joke replies must be strings" diff --git a/tests/unit/lib/pysquared/rtc/test_rp2040.py b/tests/unit/lib/pysquared/rtc/test_rp2040.py deleted file mode 100644 index 5fab6c7..0000000 --- a/tests/unit/lib/pysquared/rtc/test_rp2040.py +++ /dev/null @@ -1,41 +0,0 @@ -import time - -import pytest - -import mocks.circuitpython.rtc as MockRTC -from lib.pysquared.rtc.rp2040 import RP2040RTC - - -@pytest.fixture(autouse=True) -def cleanup(): - yield - MockRTC.RTC().destroy() - - -def test_set_time(): - """Test that the RP2040RTC.set_time method correctly sets RTC.datetime""" - year = 2025 - month = 3 - day = 6 - hour = 10 - minute = 30 - second = 45 - day_of_week = 2 - - # Set the time using the RP2040RTC class - RP2040RTC.set_time(year, month, day, hour, minute, second, day_of_week) - - # Get the mock RTC instance and check its datetime - mrtc: MockRTC = MockRTC.RTC() - assert mrtc.datetime is not None, "Mock RTC datetime should be set" - assert isinstance( - mrtc.datetime, time.struct_time - ), "Mock RTC datetime should be a time.struct_time instance" - - assert mrtc.datetime.tm_year == year, "Year should match" - assert mrtc.datetime.tm_mon == month, "Month should match" - assert mrtc.datetime.tm_mday == day, "Day should match" - assert mrtc.datetime.tm_hour == hour, "Hour should match" - assert mrtc.datetime.tm_min == minute, "Minute should match" - assert mrtc.datetime.tm_sec == second, "Second should match" - assert mrtc.datetime.tm_wday == day_of_week, "Day of week should match" diff --git a/tests/unit/lib/pysquared/rtc/test_rtc_common.py b/tests/unit/lib/pysquared/rtc/test_rtc_common.py deleted file mode 100644 index d8faf28..0000000 --- a/tests/unit/lib/pysquared/rtc/test_rtc_common.py +++ /dev/null @@ -1,23 +0,0 @@ -import time - -import pytest - -import mocks.circuitpython.rtc as MockRTC -from lib.pysquared.rtc.rtc_common import RTC - - -@pytest.fixture(autouse=True) -def cleanup(): - yield - MockRTC.RTC().destroy() - - -def test_init(): - """Test that the RTC.datetime is initialized with a time.struct_time""" - RTC.init() - - mrtc: MockRTC = MockRTC.RTC() - assert mrtc.datetime is not None, "Mock RTC datetime should be set" - assert isinstance( - mrtc.datetime, time.struct_time - ), "Mock RTC datetime should be a time.struct_time instance" diff --git a/tests/unit/lib/pysquared/test_config.py b/tests/unit/lib/pysquared/test_config.py deleted file mode 100644 index fb9bd8c..0000000 --- a/tests/unit/lib/pysquared/test_config.py +++ /dev/null @@ -1,145 +0,0 @@ -import json -import os - -from lib.pysquared.config.config import Config - -os.path.dirname(__file__) -file = f"{os.path.dirname(__file__)}/files/config.test.json" - - -def test_radio_cfg() -> None: - with open(file, "r") as f: - json_data = json.loads(f.read()) - - config = Config(file) - - # Test basic radio config properties - assert ( - config.radio.sender_id == json_data["radio"]["sender_id"] - ), "No match for: sender_id" - assert ( - config.radio.receiver_id == json_data["radio"]["receiver_id"] - ), "No match for: receiver_id" - assert ( - config.radio.transmit_frequency == json_data["radio"]["transmit_frequency"] - ), "No match for: transmit_frequency" - assert ( - config.radio.start_time == json_data["radio"]["start_time"] - ), "No match for: start_time" - - # Test FSK config properties - assert ( - config.radio.fsk.broadcast_address - == json_data["radio"]["fsk"]["broadcast_address"] - ), "No match for: fsk.broadcast_address" - assert ( - config.radio.fsk.node_address == json_data["radio"]["fsk"]["node_address"] - ), "No match for: fsk.node_address" - assert ( - config.radio.fsk.modulation_type == json_data["radio"]["fsk"]["modulation_type"] - ), "No match for: fsk.modulation_type" - - # Test LoRa config properties - assert ( - config.radio.lora.ack_delay == json_data["radio"]["lora"]["ack_delay"] - ), "No match for: lora.ack_delay" - assert ( - config.radio.lora.coding_rate == json_data["radio"]["lora"]["coding_rate"] - ), "No match for: lora.coding_rate" - assert ( - config.radio.lora.cyclic_redundancy_check - == json_data["radio"]["lora"]["cyclic_redundancy_check"] - ), "No match for: lora.cyclic_redundancy_check" - assert ( - config.radio.lora.max_output == json_data["radio"]["lora"]["max_output"] - ), "No match for: lora.max_output" - assert ( - config.radio.lora.spreading_factor - == json_data["radio"]["lora"]["spreading_factor"] - ), "No match for: lora.spreading_factor" - assert ( - config.radio.lora.transmit_power == json_data["radio"]["lora"]["transmit_power"] - ), "No match for: lora.transmit_power" - - -def test_strings() -> None: - with open(file, "r") as f: - json_data = json.loads(f.read()) - - config = Config(file) - - assert ( - config.cubesat_name == json_data["cubesat_name"] - ), "No match for: cubesat_name" - assert ( - config.super_secret_code == json_data["super_secret_code"] - ), "No match for: super_secret_code" - assert config.repeat_code == json_data["repeat_code"], "No match for: repeat_code" - - -def test_ints() -> None: - with open(file, "r") as f: - json_data = json.loads(f.read()) - - config = Config(file) - - assert ( - config.sleep_duration == json_data["sleep_duration"] - ), "No match for: sleep_duration" - assert config.normal_temp == json_data["normal_temp"], "No match for: normal_temp" - assert ( - config.normal_battery_temp == json_data["normal_battery_temp"] - ), "No match for: normal_battery_temp" - assert ( - config.normal_micro_temp == json_data["normal_micro_temp"] - ), "No match for: normal_micro_temp" - assert config.reboot_time == json_data["reboot_time"], "No match for: reboot_time" - - -def test_floats() -> None: - with open(file, "r") as f: - json_data = json.loads(f.read()) - - config = Config(file) - - assert ( - config.last_battery_temp == json_data["last_battery_temp"] - ), "No match for: last_battery_temp" - assert ( - config.normal_charge_current == json_data["normal_charge_current"] - ), "No match for: normal_charge_current" - assert ( - config.normal_battery_voltage == json_data["normal_battery_voltage"] - ), "No match for: normal_battery_voltage" - assert ( - config.critical_battery_voltage == json_data["critical_battery_voltage"] - ), "No match for: critical_battery_voltage" - assert ( - config.battery_voltage == json_data["battery_voltage"] - ), "No match for: battery_voltage" - assert ( - config.current_draw == json_data["current_draw"] - ), "No match for: current_draw" - - -def test_bools() -> None: - with open(file, "r") as f: - json_data = json.loads(f.read()) - - config = Config(file) - - assert ( - config.detumble_enable_z == json_data["detumble_enable_z"] - ), "No match for: detumble_enable_z" - assert ( - config.detumble_enable_x == json_data["detumble_enable_x"] - ), "No match for: detumble_enable_x" - assert ( - config.detumble_enable_y == json_data["detumble_enable_y"] - ), "No match for: detumble_enable_y" - assert config.debug == json_data["debug"], "No match for: debug" - assert config.legacy == json_data["legacy"], "No match for: legacy" - assert config.heating == json_data["heating"], "No match for: heating" - assert config.orpheus == json_data["orpheus"], "No match for: orpheus" - assert config.is_licensed == json_data["is_licensed"], "No match for: is_licensed" - assert config.turbo_clock == json_data["turbo_clock"], "No match for: turbo_clock" diff --git a/tests/unit/lib/pysquared/test_detumble.py b/tests/unit/lib/pysquared/test_detumble.py deleted file mode 100644 index 33ee981..0000000 --- a/tests/unit/lib/pysquared/test_detumble.py +++ /dev/null @@ -1,105 +0,0 @@ -# After following the necessary steps in README.md, you can use "make test" to run all tests in the unit_tests folder -# To run this file specifically: cd Tests > cd unit_tests > pytest test_detumble.py -# pytest test_detumble.py -v displays which tests ran and their respective results (fail or pass) -# Note: If you encounter a ModuleNotFoundError, try: export PYTHONPATH= - -import pytest - -import lib.pysquared.detumble as detumble - - -def test_dot_product(): - # dot_product is only ever called to give the square of mag_field - mag_field_vector = (30.0, 45.0, 60.0) - result = detumble.dot_product(mag_field_vector, mag_field_vector) - assert result == 6525.0 # 30.0*30.0 + 45.0*45.0 + 60.0*60.0 = 6525.0 - - -def test_dot_product_negatives(): - # testing with negative vectors - vector1 = (-1, -2, -3) - vector2 = (-4, -5, -6) - result = detumble.dot_product(vector1, vector2) - assert result == 32 # -1*-4 + -2*-5 + -3*-6 - - -def test_dot_product_large_val(): - # testing with large value vectors - vector1 = (1e6, 1e6, 1e6) - vector2 = (1e6, 1e6, 1e6) - result = detumble.dot_product(vector1, vector2) - assert result == 3e12 # 1e6*1e6 + 1e6*1e6 + 1e6*1e6 = 3e12 - - -def test_dot_product_zero(): - # testing with zero values - vector = (0.0, 0.0, 0.0) - result = detumble.dot_product(vector, vector) - assert result == 0.0 - - -def test_x_product(): - mag_field_vector = (30.0, 45.0, 60.0) - ang_vel_vector = (0.0, 0.02, 0.015) - expected_result = [-0.525, 0.45, 0.6] - # x_product takes in tuple arguments and returns a list value - actual_result = detumble.x_product( - mag_field_vector, ang_vel_vector - ) # cross product - assert pytest.approx(actual_result[0], 0.001) == expected_result[0] - assert pytest.approx(actual_result[1], 0.001) == expected_result[1] - assert pytest.approx(actual_result[2], 0.001) == expected_result[2] - # due to floating point arithmetic, accept answer within 5 places - - -def test_x_product_negatives(): - mag_field_vector = (-30.0, -45.0, -60.0) - ang_vel_vector = (-0.02, -0.02, -0.015) - expected_result = [-0.525, -0.75, -0.3] - actual_result = detumble.x_product(mag_field_vector, ang_vel_vector) - assert pytest.approx(actual_result[0], 0.001) == expected_result[0] - assert pytest.approx(actual_result[1], 0.001) == expected_result[1] - assert pytest.approx(actual_result[2], 0.001) == expected_result[2] - - -def test_x_product_large_val(): - mag_field_vector = (1e6, 1e6, 1e6) - ang_vel_vector = (1e6, 1e6, 1e6) # cross product of parallel vector equals 0 - result = detumble.x_product(mag_field_vector, ang_vel_vector) - assert result == [0.0, 0.0, 0.0] - - -def test_x_product_zero(): - mag_field_vector = (0.0, 0.0, 0.0) - ang_vel_vector = (0.0, 0.02, 0.015) - result = detumble.x_product(mag_field_vector, ang_vel_vector) - assert result == [0.0, 0.0, 0.0] - - -# Bigger context: magnetorquer_dipole() is called by do_detumble() in (FC board) functions.py & (Batt Board) battery_functions.py -# mag_field: mag. field strength at x, y, & z axis (tuple) (magnetometer reading) -# ang_vel: ang. vel. at x, y, z axis (tuple) (gyroscope reading) -def test_magnetorquer_dipole(): - mag_field = (30.0, -45.0, 60.0) - ang_vel = (0.0, 0.02, 0.015) - expected_result = [0.023211, -0.00557, -0.007426] - actual_result = detumble.magnetorquer_dipole(mag_field, ang_vel) - assert pytest.approx(actual_result[0], 0.001) == expected_result[0] - assert pytest.approx(actual_result[1], 0.001) == expected_result[1] - assert pytest.approx(actual_result[2], 0.001) == expected_result[2] - - -def test_magnetorquer_dipole_zero_mag_field(): - # testing throwing of exception when mag_field = 0 (division by 0) - mag_field = (0.0, 0.0, 0.0) - ang_vel = (0.0, 0.02, 0.015) - with pytest.raises(ZeroDivisionError): - detumble.magnetorquer_dipole(mag_field, ang_vel) - - -def test_magnetorquer_dipole_zero_ang_vel(): - # testing ang_vel with zero value - mag_field = (30.0, -45.0, 60.0) - ang_vel = (0.0, 0.0, 0.0) - result = detumble.magnetorquer_dipole(mag_field, ang_vel) - assert result == [0.0, 0.0, 0.0] diff --git a/tests/unit/lib/pysquared/test_logger.py b/tests/unit/lib/pysquared/test_logger.py deleted file mode 100644 index f5adad9..0000000 --- a/tests/unit/lib/pysquared/test_logger.py +++ /dev/null @@ -1,189 +0,0 @@ -import pytest - -import lib.pysquared.nvm.counter as counter -from lib.pysquared.logger import Logger, _color -from mocks.circuitpython.byte_array import ByteArray - - -@pytest.fixture -def logger(): - datastore = ByteArray(size=8) - index = 0 - count = counter.Counter(index, datastore) - return Logger(count) - - -@pytest.fixture -def logger_color(): - datastore = ByteArray(size=8) - index = 0 - count = counter.Counter(index, datastore) - return Logger(error_counter=count, colorized=True) - - -def test_debug_log(capsys, logger): - logger.debug("This is a debug message", blake="jameson") - captured = capsys.readouterr() - assert "DEBUG" in captured.out - assert "This is a debug message" in captured.out - assert '"blake": "jameson"' in captured.out - - -def test_debug_with_err(capsys, logger): - logger.debug( - "This is another debug message", err=OSError("Manually creating an OS Error") - ) - captured = capsys.readouterr() - assert "DEBUG" in captured.out - assert "This is another debug message" in captured.out - assert "OSError: Manually creating an OS Error" in captured.out - - -def test_info_log(capsys, logger): - logger.info( - "This is a info message!!", - foo="bar", - ) - captured = capsys.readouterr() - assert "INFO" in captured.out - assert "This is a info message!!" in captured.out - assert '"foo": "bar"' in captured.out - - -def test_info_with_err(capsys, logger): - logger.info( - "This is a info message!!", - foo="barrrr", - err=OSError("Manually creating an OS Error"), - ) - captured = capsys.readouterr() - assert "INFO" in captured.out - assert "This is a info message!!" in captured.out - assert '"foo": "barrrr"' in captured.out - assert "OSError: Manually creating an OS Error" in captured.out - - -def test_warning_log(capsys, logger): - logger.warning( - "This is a warning message!!??!", - boo="bar", - pleiades="maia", - cube="sat", - err=Exception("manual exception"), - ) - captured = capsys.readouterr() - assert "WARNING" in captured.out - assert "This is a warning message!!??!" in captured.out - assert '"boo": "bar"' in captured.out - assert '"pleiades": "maia"' in captured.out - assert '"cube": "sat"' in captured.out - assert "Exception: manual exception" in captured.out - - -def test_error_log(capsys, logger): - logger.error( - "This is an error message", - OSError("Manually creating an OS Error for testing"), - hee="haa", - pleiades="five", - please="work", - ) - captured = capsys.readouterr() - assert "ERROR" in captured.out - assert "This is an error message" in captured.out - assert '"hee": "haa"' in captured.out - assert '"pleiades": "five"' in captured.out - assert '"please": "work"' in captured.out - assert "OSError: Manually creating an OS Error for testing" in captured.out - - -def test_critical_log(capsys, logger): - logger.critical( - "THIS IS VERY CRITICAL", - OSError("Manually creating an OS Error"), - ad="astra", - space="lab", - soft="ware", - j="20", - config="king", - ) - captured = capsys.readouterr() - assert "CRITICAL" in captured.out - assert "THIS IS VERY CRITICAL" in captured.out - assert '"ad": "astra"' in captured.out - assert '"space": "lab"' in captured.out - assert '"soft": "ware"' in captured.out - assert '"j": "20"' in captured.out - assert '"config": "king"' in captured.out - - -def test_type_error_log(capsys, logger): - logger.info("Testing type error", bad_arg=lambda x: x + 1) - captured = capsys.readouterr() - assert '"level": "ERROR"' in captured.out - assert "Failed to serialize log message:" in captured.out - assert "Object of type function is not JSON serializable" in captured.out - - -def test_debug_log_color(capsys, logger_color): - logger_color.debug("This is a debug message", blake="jameson") - captured = capsys.readouterr() - assert _color(msg="DEBUG", color="blue") in captured.out - assert "This is a debug message" in captured.out - assert '"blake": "jameson"' in captured.out - - -def test_info_log_color(capsys, logger_color): - logger_color.info("This is a info message!!", foo="bar") - captured = capsys.readouterr() - assert _color(msg="INFO", color="green") in captured.out - assert "This is a info message!!" in captured.out - assert '"foo": "bar"' in captured.out - - -def test_warning_log_color(capsys, logger_color): - logger_color.warning( - "This is a warning message!!??!", boo="bar", pleiades="maia", cube="sat" - ) - captured = capsys.readouterr() - assert _color(msg="WARNING", color="orange") in captured.out - assert "This is a warning message!!??!" in captured.out - assert '"boo": "bar"' in captured.out - assert '"pleiades": "maia"' in captured.out - assert '"cube": "sat"' in captured.out - - -def test_error_log_color(capsys, logger_color): - logger_color.error( - "This is an error message", - hee="haa", - pleiades="five", - please="work", - err=OSError("Manually creating an OS Error"), - ) - captured = capsys.readouterr() - assert _color(msg="ERROR", color="pink") in captured.out - assert "This is an error message" in captured.out - assert '"hee": "haa"' in captured.out - assert '"pleiades": "five"' in captured.out - assert '"please": "work"' in captured.out - - -def test_critical_log_color(capsys, logger_color): - logger_color.critical( - "THIS IS VERY CRITICAL", - ad="astra", - space="lab", - soft="ware", - j="20", - config="king", - err=OSError("Manually creating an OS Error"), - ) - captured = capsys.readouterr() - assert _color(msg="CRITICAL", color="red") in captured.out - assert "THIS IS VERY CRITICAL" in captured.out - assert '"ad": "astra"' in captured.out - assert '"space": "lab"' in captured.out - assert '"soft": "ware"' in captured.out - assert '"j": "20"' in captured.out - assert '"config": "king"' in captured.out