From 1291bdff30d0e31704292925305387f203b4fed4 Mon Sep 17 00:00:00 2001 From: Nate Gay Date: Wed, 18 Dec 2024 18:14:08 -0600 Subject: [PATCH] Merge Orpheus edition into main (#55) Co-authored-by: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Co-authored-by: DeathStar Co-authored-by: DeathStar Co-authored-by: Cameron <57497866+Camerooooon@users.noreply.github.com> Co-authored-by: Alex Mariano <65744075+Lex-ari@users.noreply.github.com> --- Batt_Board/can_test.py | 6 +- Batt_Board/code.py | 6 +- Batt_Board/lib/Big_Data.py | 365 ---- Batt_Board/lib/adafruit_max31856.py | 451 +++++ Batt_Board/lib/adafruit_mcp2515/__init__.py | 4 + Batt_Board/lib/adafruit_register/__init__.py | 0 .../lib/adafruit_register/i2c_bcd_alarm.mpy | Bin 0 -> 1740 bytes .../lib/adafruit_register/i2c_bcd_alarm.py | 198 +++ .../adafruit_register/i2c_bcd_datetime.mpy | Bin 0 -> 1141 bytes .../lib/adafruit_register/i2c_bcd_datetime.py | 114 ++ Batt_Board/lib/adafruit_register/i2c_bit.mpy | Bin 0 -> 801 bytes Batt_Board/lib/adafruit_register/i2c_bit.py | 84 + Batt_Board/lib/adafruit_register/i2c_bits.mpy | Bin 0 -> 1053 bytes Batt_Board/lib/adafruit_register/i2c_bits.py | 114 ++ .../lib/adafruit_register/i2c_struct.mpy | Bin 0 -> 1040 bytes .../lib/adafruit_register/i2c_struct.py | 104 ++ .../adafruit_register/i2c_struct_array.mpy | Bin 0 -> 1008 bytes .../lib/adafruit_register/i2c_struct_array.py | 114 ++ Batt_Board/lib/adafruit_vl6180x.py | 411 +++++ Batt_Board/lib/battery_functions.py | 18 - Batt_Board/lib/neopixel.mpy | Bin 0 -> 1318 bytes Batt_Board/lib/pysquared_eps.py | 346 +++- FC_Board/cdh.py | 11 +- FC_Board/code.py | 15 +- FC_Board/lib/Big_Data.py | 95 + FC_Board/lib/Field.py | 22 +- FC_Board/lib/adafruit_mcp2515/__init__.py | 943 ++++++++++ .../lib/adafruit_mcp2515/canio/__init__.py | 220 +++ FC_Board/lib/adafruit_mcp2515/timer.py | 29 + FC_Board/lib/adafruit_ov5640/__init__.py | 1583 +++++++++++++++++ .../lib/adafruit_ov5640/ov5640_autofocus.bin | Bin 0 -> 4077 bytes .../ov5640_autofocus.bin.license | 3 + FC_Board/lib/adafruit_pca9685.py | 189 ++ FC_Board/lib/adafruit_rfm/rfm9x.py | 535 ++++++ FC_Board/lib/adafruit_rfm/rfm9xfsk.py | 578 ++++++ FC_Board/lib/adafruit_rfm/rfm_common.py | 586 ++++++ .../lib/adafruit_tca9548a.py | 4 +- FC_Board/lib/battery_helper.py | 293 +++ FC_Board/lib/can_bus_helper.py | 462 +++++ FC_Board/lib/functions.py | 188 +- FC_Board/lib/packet_manager.py | 117 ++ FC_Board/lib/packet_sender.py | 165 ++ FC_Board/lib/pysquared.py | 410 ++++- FC_Board/lib/pysquared_rfm9x.py | 1116 ------------ FC_Board/lib/rv3028.py | 239 +++ FC_Board/main.py | 200 +-- FC_Board/repl.py | 7 + Tests/fsk_test.py | 61 + Tests/radio_test.py | 128 +- experimental/lib/packet_receiver.py | 381 ++++ 50 files changed, 9020 insertions(+), 1895 deletions(-) delete mode 100644 Batt_Board/lib/Big_Data.py create mode 100644 Batt_Board/lib/adafruit_max31856.py create mode 100644 Batt_Board/lib/adafruit_register/__init__.py create mode 100644 Batt_Board/lib/adafruit_register/i2c_bcd_alarm.mpy create mode 100644 Batt_Board/lib/adafruit_register/i2c_bcd_alarm.py create mode 100644 Batt_Board/lib/adafruit_register/i2c_bcd_datetime.mpy create mode 100644 Batt_Board/lib/adafruit_register/i2c_bcd_datetime.py create mode 100644 Batt_Board/lib/adafruit_register/i2c_bit.mpy create mode 100644 Batt_Board/lib/adafruit_register/i2c_bit.py create mode 100644 Batt_Board/lib/adafruit_register/i2c_bits.mpy create mode 100644 Batt_Board/lib/adafruit_register/i2c_bits.py create mode 100644 Batt_Board/lib/adafruit_register/i2c_struct.mpy create mode 100644 Batt_Board/lib/adafruit_register/i2c_struct.py create mode 100644 Batt_Board/lib/adafruit_register/i2c_struct_array.mpy create mode 100644 Batt_Board/lib/adafruit_register/i2c_struct_array.py create mode 100644 Batt_Board/lib/adafruit_vl6180x.py create mode 100755 Batt_Board/lib/neopixel.mpy create mode 100755 FC_Board/lib/Big_Data.py create mode 100644 FC_Board/lib/adafruit_mcp2515/__init__.py create mode 100644 FC_Board/lib/adafruit_mcp2515/canio/__init__.py create mode 100644 FC_Board/lib/adafruit_mcp2515/timer.py create mode 100644 FC_Board/lib/adafruit_ov5640/__init__.py create mode 100644 FC_Board/lib/adafruit_ov5640/ov5640_autofocus.bin create mode 100644 FC_Board/lib/adafruit_ov5640/ov5640_autofocus.bin.license create mode 100644 FC_Board/lib/adafruit_pca9685.py create mode 100644 FC_Board/lib/adafruit_rfm/rfm9x.py create mode 100644 FC_Board/lib/adafruit_rfm/rfm9xfsk.py create mode 100644 FC_Board/lib/adafruit_rfm/rfm_common.py rename {Batt_Board => FC_Board}/lib/adafruit_tca9548a.py (97%) create mode 100644 FC_Board/lib/battery_helper.py create mode 100644 FC_Board/lib/can_bus_helper.py create mode 100644 FC_Board/lib/packet_manager.py create mode 100644 FC_Board/lib/packet_sender.py delete mode 100755 FC_Board/lib/pysquared_rfm9x.py create mode 100644 FC_Board/lib/rv3028.py mode change 100644 => 100755 FC_Board/main.py create mode 100644 FC_Board/repl.py create mode 100644 Tests/fsk_test.py mode change 100644 => 100755 Tests/radio_test.py create mode 100644 experimental/lib/packet_receiver.py diff --git a/Batt_Board/can_test.py b/Batt_Board/can_test.py index 71cca20e..42180a50 100644 --- a/Batt_Board/can_test.py +++ b/Batt_Board/can_test.py @@ -1,9 +1,9 @@ -from pysquared_eps import cubesat as c -import battery_functions +from pysquared import cubesat as c +import functions import can_bus_helper import time -f = battery_functions.functions(c) +f = functions.functions(c) cb = can_bus_helper.CanBusHelper(c.can_bus, f, True) diff --git a/Batt_Board/code.py b/Batt_Board/code.py index 36c3fb6d..332191bd 100644 --- a/Batt_Board/code.py +++ b/Batt_Board/code.py @@ -13,6 +13,6 @@ except Exception as e: print(e) - time.sleep(10) - microcontroller.on_next_reset(microcontroller.RunMode.NORMAL) - microcontroller.reset() + # time.sleep(10) + # microcontroller.on_next_reset(microcontroller.RunMode.NORMAL) + # microcontroller.reset() diff --git a/Batt_Board/lib/Big_Data.py b/Batt_Board/lib/Big_Data.py deleted file mode 100644 index 355df0df..00000000 --- a/Batt_Board/lib/Big_Data.py +++ /dev/null @@ -1,365 +0,0 @@ -""" -This class creates an object for each face of Yearling. - -Authors: Antony Macar, Michael Pham, Nicole Maggard -Updated July 26, 2022 -v1.1 -""" - -from debugcolor import co -import time -import board -import busio -import traceback -import adafruit_mcp9808 # temperature sensor -import adafruit_tca9548a # I2C multiplexer -import adafruit_veml7700 # light sensor -import adafruit_drv2605 # Coil motor driver - - -# Is the Face cass even necessary? -class Face: - - def debug_print(self, statement): - if self.debug: - print(co("[BATTERY][BIG_DATA]" + statement, "teal", "bold")) - - def __init__(self, Add, Pos, debug_state, tca): - self.tca = tca - self.address = Add - self.position = Pos - self.debug = debug_state - - # Sensor List Contains Expected Sensors Based on Face - self.senlist = [] - self.datalist = [] # [temp light mag accel gyro motordriver thermo] - if Pos == "x+": - self.senlist.append("MCP") - self.senlist.append("VEML") - self.senlist.append("DRV") - elif Pos == "x-": - self.senlist.append("MCP") - self.senlist.append("VEML") - elif Pos == "y+": - self.senlist.append("MCP") - self.senlist.append("VEML") - self.senlist.append("DRV") - elif Pos == "y-": - self.senlist.append("MCP") - self.senlist.append("VEML") - elif Pos == "z-": - self.senlist.append("MCP") - self.senlist.append("VEML") - self.senlist.append("DRV") - else: - self.debug_print("[ERROR] Please input a proper face") - - # This sensors set contains information as to whether sensors are actually working - self.sensors = { - "MCP": False, - "VEML": False, - "DRV": False, - } - - @property - def debug_value(self): - return self.debug - - @debug_value.setter - def debug_value(self, value): - self.debug = value - - # function to initialize all the sensors on that face - def Sensorinit(self, senlist, address): - - # Initialize Temperature Sensor - if "MCP" in senlist: - try: - self.mcp = adafruit_mcp9808.MCP9808(self.tca[address], address=27) - self.sensors["MCP"] = True - self.debug_print("[ACTIVE][Temperature Sensor]") - except Exception as e: - self.debug_print( - "[ERROR][Temperature Sensor]" - + "".join(traceback.format_exception(e)) - ) - - # Initialize Light Sensor - if "VEML" in senlist: - try: - self.veml = adafruit_veml7700.VEML7700(self.tca[address]) - # self.light1.enable_color =True - # self.light1.enable_proximity = True - self.sensors["VEML"] = True - self.debug_print("[ACTIVE][Light Sensor]") - except Exception as e: - self.debug_print( - "[ERROR][Light Sensor]" + "".join(traceback.format_exception(e)) - ) - - # Initialize Motor Driver - if "DRV" in senlist: - try: - self.drv = adafruit_drv2605.DRV2605(self.tca[address]) - self.sensors["DRV"] = True - self.debug_print("[ACTIVE][Motor Driver]") - except Exception as e: - self.debug_print( - "[ERROR][Motor Driver]" + "".join(traceback.format_exception(e)) - ) - - self.debug_print("Initialization Complete") - - # Meta Info Getters - @property # Gives what sensors should be present - def senlist_what(self): - return self.senlist - - @property # Givens what sensors are actually present - def active_sensors(self): - return self.sensors - - # Sensor Data Getters - - @property # Temperature Data Getter - def temperature(self): - if self.sensors["MCP"]: - return self.mcp.temperature - else: - self.debug_print("[WARNING]Temperature sensor not initialized") - - @property # Light Sensor Color Data Getter - def lux_data(self): - if self.sensors["VEML"]: - return self.veml.lux - else: - self.debug_print("[WARNING]Light sensor not initialized") - - def drv_actuate(self, duration): - if self.sensors["DRV"]: - self.debug_print("Actuating Sequence") - self.debug_print("Playing effect #{0}".format(self.drv)) - self.drv.play() - time.sleep(duration) - self.drv.stop() - self.debug_print("Actuation Complete") - else: - self.debug_print("[WARNING]Motor driver not initialized") - - @property # driver sequence Getter - def drive(self): - if self.sensors["DRV"]: - return self.drv.sequence[0] - else: - self.debug_print("[WARNING]Motor driver not initialized") - - @drive.setter # setter - def drive(self, sequence): - if self.sensors["DRV"]: - try: - self.debug_print("Encoding Sequence") - self.drv.sequence[0] = adafruit_drv2605.Effect(sequence) - self.debug_print("Complete") - except Exception as e: - self.debug_print( - "[ERROR][Motor Driver]" + "".join(traceback.format_exception(e)) - ) - else: - self.debug_print("[WARNING]Motor driver not initialized") - - # Function to test all sensors that should be on each face. - # Function takes number of tests "num" and polling rate in hz "rate" - def test_all(self, num, rate): - self.datalist = [] - self.debug_print("Expected Sensors: " + str(self.senlist_what)) - self.debug_print("Initialized Sensors: " + str(self.active_sensors)) - # time.sleep(1) #Remove later for performance boost! - self.debug_print("Initializing Test") - - for i in range(num): - - self.debug_print("Test Number: {}/{}".format(i + 1, num)) - - # Test Temperature Sensor - self.debug_print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - if ("MCP" in self.senlist) and (self.sensors.get("MCP") == True): - try: - self.debug_print("Temperature Sensor") - self.debug_print("Face Temperature: {}C".format(self.temperature)) - self.datalist.append(self.temperature) - except Exception as e: - self.debug_print( - "[ERROR][Temperature Sensor]" - + "".join(traceback.format_exception(e)) - ) - else: - self.debug_print("[ERROR]Temperature Sensor Failure") - self.datalist.append(None) - - self.debug_print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - # Test Light Sensor - if ("VEML" in self.senlist) and (self.sensors.get("VEML") == True): - try: - self.debug_print("Light Sensor") - self.debug_print("Face light: {}Lumens/Sq.ft".format(self.lux_data)) - self.datalist.append(self.lux_data) - except Exception as e: - self.debug_print( - "[ERROR][Light Sensor]" + "".join(traceback.format_exception(e)) - ) - else: - self.debug_print("[ERROR]Light Sensor Failure") - self.datalist.append(None) - - self.debug_print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - - # Test Motor Driver - if ("DRV" in self.senlist) and (self.sensors.get("DRV") == True): - try: - self.debug_print("Motor Driver") - self.debug_print( - "[ACTIVE][Motor Driver]" - ) # No function defined here yet to use the driver - except Exception as e: - self.debug_print( - "[ERROR][Motor Driver]" + "".join(traceback.format_exception(e)) - ) - self.debug_print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - else: - pass - - self.debug_print("=======================================") - # time.sleep(rate) #Remove later for performance boost! - return self.datalist - - def __del__(self): - self.debug_print("Object Destroyed!") - - -class AllFaces: - def debug_print(self, statement): - if self.debug: - print(co("[BIG_DATA]" + statement, "teal", "bold")) - - def __init__(self, debug, tca): - self.tca = tca - # Create the TCA9548A object and give it the I2C1 bus - self.debug = debug - self.debug_print("Creating Face Objects...") - self.BigFaceList = [] - self.Face4 = Face(4, "z-", self.debug, self.tca) - self.Face3 = Face(3, "x-", self.debug, self.tca) - self.Face2 = Face(2, "x+", self.debug, self.tca) - self.Face1 = Face(1, "y-", self.debug, self.tca) - self.Face0 = Face(0, "y+", self.debug, self.tca) - self.debug_print("Done!") - - # Initialize All Faces - try: - self.Face0.Sensorinit(self.Face0.senlist, self.Face0.address) - except Exception as e: - self.debug_print( - "[ERROR][Face0 Initialization]" + "".join(traceback.format_exception(e)) - ) - - try: - self.Face1.Sensorinit(self.Face1.senlist, self.Face1.address) - except Exception as e: - self.debug_print( - "[ERROR][Face1 Initialization]" + "".join(traceback.format_exception(e)) - ) - - try: - self.Face2.Sensorinit(self.Face2.senlist, self.Face2.address) - except Exception as e: - self.debug_print( - "[ERROR][Face2 Initialization]" + "".join(traceback.format_exception(e)) - ) - - try: - self.Face3.Sensorinit(self.Face3.senlist, self.Face3.address) - except Exception as e: - self.debug_print( - "[ERROR][Face3 Initialization]" + "".join(traceback.format_exception(e)) - ) - - try: - self.Face4.Sensorinit(self.Face4.senlist, self.Face4.address) - except Exception as e: - self.debug_print( - "[ERROR][Face4 Initialization]" + "".join(traceback.format_exception(e)) - ) - - self.debug_print("Faces Initialized") - - @property # driver sequence Getter - def sequence(self): - return self.Face0.drive, self.Face2.drive, self.Face4.drive - - @sequence.setter # setter - def sequence(self, seq): - self.Face0.drive = seq - self.Face2.drive = seq - self.Face4.drive = seq - - def driver_actuate(self, duration): - try: - self.Face0.drv_actuate(duration) - self.Face2.drv_actuate(duration) - self.Face4.drv_actuate(duration) - except Exception as e: - self.debug_print( - "Driver Test error: " + "".join(traceback.format_exception(e)) - ) - - def drvx_actuate(self, duration): - try: - self.Face2.drv_actuate(duration) - except Exception as e: - self.debug_print( - "Driver Test error: " + "".join(traceback.format_exception(e)) - ) - - def drvy_actuate(self, duration): - try: - self.Face0.drv_actuate(duration) - except Exception as e: - self.debug_print( - "Driver Test error: " + "".join(traceback.format_exception(e)) - ) - - def drvz_actuate(self, duration): - try: - self.Face4.drv_actuate(duration) - except Exception as e: - self.debug_print( - "Driver Test error: " + "".join(traceback.format_exception(e)) - ) - - # Function that polls all of the sensors on all of the faces one time and prints the results. - def Face_Test_All(self): - try: - self.BigFaceList = [] - self.debug_print("Creating Face List") - self.BigFaceList.append(self.Face0.test_all(1, 0.1)) - self.BigFaceList.append(self.Face1.test_all(1, 0.1)) - self.BigFaceList.append(self.Face2.test_all(1, 0.1)) - self.BigFaceList.append(self.Face3.test_all(1, 0.1)) - self.BigFaceList.append(self.Face4.test_all(1, 0.1)) - - for face in self.BigFaceList: - self.debug_print(str(face)) - - except Exception as e: - self.debug_print( - "All Face test error:" + "".join(traceback.format_exception(e)) - ) - return self.BigFaceList - - def __del__(self): - del self.Face0 - del self.Face1 - del self.Face2 - del self.Face3 - del self.Face4 - self.debug_print("Object Destroyed!") diff --git a/Batt_Board/lib/adafruit_max31856.py b/Batt_Board/lib/adafruit_max31856.py new file mode 100644 index 00000000..4427d4b3 --- /dev/null +++ b/Batt_Board/lib/adafruit_max31856.py @@ -0,0 +1,451 @@ +# SPDX-FileCopyrightText: 2018 Bryan Siepert for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`MAX31856` +==================================================== + +CircuitPython module for the MAX31856 Universal Thermocouple Amplifier. See +examples/simpletest.py for an example of the usage. + +* Author(s): Bryan Siepert + +Implementation Notes +-------------------- + +**Hardware:** + +* Adafruit `Universal Thermocouple Amplifier MAX31856 Breakout + `_ (Product ID: 3263) + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice + +""" + +from time import sleep +from micropython import const +from adafruit_bus_device.spi_device import SPIDevice + +try: + from typing import Dict, Tuple + from typing_extensions import Literal + from busio import SPI + from digitalio import DigitalInOut +except ImportError: + pass + +try: + from struct import unpack +except ImportError: + from ustruct import unpack + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MAX31856.git" + +# Register constants +_MAX31856_CR0_REG = const(0x00) +_MAX31856_CR0_AUTOCONVERT = const(0x80) +_MAX31856_CR0_1SHOT = const(0x40) +_MAX31856_CR0_OCFAULT1 = const(0x20) +_MAX31856_CR0_OCFAULT0 = const(0x10) +_MAX31856_CR0_CJ = const(0x08) +_MAX31856_CR0_FAULT = const(0x04) +_MAX31856_CR0_FAULTCLR = const(0x02) +_MAX31856_CR0_50HZ = const(0x01) + +_MAX31856_CR1_REG = const(0x01) +_MAX31856_MASK_REG = const(0x02) +_MAX31856_CJHF_REG = const(0x03) +_MAX31856_CJLF_REG = const(0x04) +_MAX31856_LTHFTH_REG = const(0x05) +_MAX31856_LTHFTL_REG = const(0x06) +_MAX31856_LTLFTH_REG = const(0x07) +_MAX31856_LTLFTL_REG = const(0x08) +_MAX31856_CJTO_REG = const(0x09) +_MAX31856_CJTH_REG = const(0x0A) +_MAX31856_CJTL_REG = const(0x0B) +_MAX31856_LTCBH_REG = const(0x0C) +_MAX31856_LTCBM_REG = const(0x0D) +_MAX31856_LTCBL_REG = const(0x0E) +_MAX31856_SR_REG = const(0x0F) + +# fault types +_MAX31856_FAULT_CJRANGE = const(0x80) +_MAX31856_FAULT_TCRANGE = const(0x40) +_MAX31856_FAULT_CJHIGH = const(0x20) +_MAX31856_FAULT_CJLOW = const(0x10) +_MAX31856_FAULT_TCHIGH = const(0x08) +_MAX31856_FAULT_TCLOW = const(0x04) +_MAX31856_FAULT_OVUV = const(0x02) +_MAX31856_FAULT_OPEN = const(0x01) + +_AVGSEL_CONSTS = {1: 0x00, 2: 0x10, 4: 0x20, 8: 0x30, 16: 0x40} + + +class ThermocoupleType: # pylint: disable=too-few-public-methods + """An enum-like class representing the different types of thermocouples that the MAX31856 can + use. The values can be referenced like ``ThermocoupleType.K`` or ``ThermocoupleType.S`` + Possible values are + + - ``ThermocoupleType.B`` + - ``ThermocoupleType.E`` + - ``ThermocoupleType.J`` + - ``ThermocoupleType.K`` + - ``ThermocoupleType.N`` + - ``ThermocoupleType.R`` + - ``ThermocoupleType.S`` + - ``ThermocoupleType.T`` + + """ + + # pylint: disable=invalid-name + B = 0b0000 + E = 0b0001 + J = 0b0010 + K = 0b0011 + N = 0b0100 + R = 0b0101 + S = 0b0110 + T = 0b0111 + G8 = 0b1000 + G32 = 0b1100 + + +class MAX31856: + """Driver for the MAX31856 Universal Thermocouple Amplifier + + :param ~busio.SPI spi: The SPI bus the MAX31856 is connected to. + :param ~microcontroller.Pin cs: The pin used for the CS signal. + :param ~adafruit_max31856.ThermocoupleType thermocouple_type: The type of thermocouple.\ + Default is Type K. + :param ~int baudrate: The SPI baudrate. Default is 500000. + + **Quickstart: Importing and using the MAX31856** + + Here is an example of using the :class:`MAX31856` class. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + import board + from digitalio import DigitalInOut, Direction + import adafruit_max31856 + + Once this is done you can define your `board.SPI` object and define your sensor object + + .. code-block:: python + + spi = board.SPI() + cs = digitalio.DigitalInOut(board.D5) # Chip select of the MAX31856 board. + sensor = adafruit_max31856.MAX31856(spi, cs) + + + Now you have access to the :attr:`temperature` attribute + + .. code-block:: python + + temperature = sensor.temperature + + """ + + # A class level buffer to reduce allocations for reading and writing. + # Tony says this isn't re-entrant or thread safe! + _BUFFER = bytearray(4) + + def __init__( + self, + spi: SPI, + cs: DigitalInOut, # pylint: disable=invalid-name + thermocouple_type: int = ThermocoupleType.K, + baudrate: int = 500000, + ) -> None: + self._device = SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=1) + + # assert on any fault + self._write_u8(_MAX31856_MASK_REG, 0x0) + # configure open circuit faults + self._write_u8(_MAX31856_CR0_REG, _MAX31856_CR0_OCFAULT0) + + # set thermocouple type + self._set_thermocouple_type(thermocouple_type) + + def _set_thermocouple_type(self, thermocouple_type: ThermocoupleType) -> None: + # get current value of CR1 Reg + conf_reg_1 = self._read_register(_MAX31856_CR1_REG, 1)[0] + conf_reg_1 &= 0xF0 # mask off bottom 4 bits + # add the new value for the TC type + conf_reg_1 |= int(thermocouple_type) & 0x0F + self._write_u8(_MAX31856_CR1_REG, conf_reg_1) + + @property + def averaging(self) -> int: + """Number of samples averaged together in each result. + Must be 1, 2, 4, 8, or 16. Default is 1 (no averaging). + """ + conf_reg_1 = self._read_register(_MAX31856_CR1_REG, 1)[0] + avgsel = conf_reg_1 & ~0b10001111 # clear bits other than 4-6 + # check which byte this corresponds to + for key, value in _AVGSEL_CONSTS.items(): + if value == avgsel: + return key + raise KeyError(f"AVGSEL bit pattern was not recognised ({avgsel:>08b})") + + @averaging.setter + def averaging(self, num_samples: int) -> None: + # This option is set in bits 4-6 of register CR1. + if num_samples not in _AVGSEL_CONSTS: + raise ValueError("Num_samples must be one of 1,2,4,8,16") + avgsel = _AVGSEL_CONSTS[num_samples] + # get current value of CR1 Reg + conf_reg_1 = self._read_register(_MAX31856_CR1_REG, 1)[0] + conf_reg_1 &= 0b10001111 # clear bits 4-6 + # OR the AVGSEL bits (4-6) + conf_reg_1 |= avgsel + self._write_u8(_MAX31856_CR1_REG, conf_reg_1) + + @property + def noise_rejection(self) -> Literal[50, 60]: + """ + The frequency (Hz) to be used by the noise rejection filter. + Must be 50 or 60. Default is 60.""" + # this value is stored in bit 0 of register CR0. + conf_reg_0 = self._read_register(_MAX31856_CR0_REG, 1)[0] + if conf_reg_0 & _MAX31856_CR0_50HZ: + return 50 + return 60 + + @noise_rejection.setter + def noise_rejection(self, frequency: Literal[50, 60]) -> None: + conf_reg_0 = self._read_register(_MAX31856_CR0_REG, 1)[0] + if frequency == 50: + conf_reg_0 |= _MAX31856_CR0_50HZ # set the 50hz bit + elif frequency == 60: + conf_reg_0 &= ~_MAX31856_CR0_50HZ # clear the 50hz bit + else: + raise ValueError("Frequency must be 50 or 60") + self._write_u8(_MAX31856_CR0_REG, conf_reg_0) + + @property + def temperature(self) -> float: + """Measure the temperature of the sensor and wait for the result. + Return value is in degrees Celsius. (read-only)""" + self._perform_one_shot_measurement() + return self.unpack_temperature() + + def unpack_temperature(self) -> float: + """Reads the probe temperature from the register""" + # unpack the 3-byte temperature as 4 bytes + raw_temp = unpack( + ">i", self._read_register(_MAX31856_LTCBH_REG, 3) + bytes([0]) + )[0] + + # shift to remove extra byte from unpack needing 4 bytes + raw_temp >>= 8 + + # effectively shift raw_read >> 12 to convert pseudo-float + temp_float = raw_temp / 4096.0 + + return temp_float + + def read_high_res_temp(self) -> float: + """Reads 19-bit temperature data from the sensor and returns it in degrees Celsius. + + Reading must have already been initiated via: + `initiate_one_shot_measurement` or `start_autoconverting`. + + Returns: + float: temperature in degrees Celsius + """ + # Per datasheet, temperature resolution in °C per LSB + resolution = 0.0078125 + + # Read the temperature registers + raw_bytes = self._read_sequential_registers(_MAX31856_LTCBH_REG, 3) + # Extract individual bytes from the byte array + high_byte = raw_bytes[0] # First byte + mid_byte = raw_bytes[1] # Second byte + low_byte = raw_bytes[2] # Third byte + + # Combine the bytes into a single 19-bit value + combined = (high_byte << 11) | (mid_byte << 3) | (low_byte >> 5) + + # Adjust for two's complement (sign extension for negative values) + if combined & 0x40000: # Check if 19th bit is set (negative temperature) + combined = combined - 0x80000 + + # Convert to temperature using the resolution + return combined * resolution + + @property + def reference_temperature(self) -> float: + """Wait to retrieve temperature of the cold junction in degrees Celsius. (read-only)""" + self._perform_one_shot_measurement() + return self.unpack_reference_temperature() + + def unpack_reference_temperature(self) -> float: + """Reads the reference temperature from the register""" + raw_read = unpack(">h", self._read_register(_MAX31856_CJTH_REG, 2))[0] + + # effectively shift raw_read >> 8 to convert pseudo-float + cold_junction_temp = raw_read / 256.0 + + return cold_junction_temp + + @property + def temperature_thresholds(self) -> Tuple[float, float]: + """The thermocouple's low and high temperature thresholds + as a ``(low_temp, high_temp)`` tuple + """ + + raw_low = unpack(">h", self._read_register(_MAX31856_LTLFTH_REG, 2)) + raw_high = unpack(">h", self._read_register(_MAX31856_LTHFTH_REG, 2)) + + return (round(raw_low[0] / 16.0, 1), round(raw_high[0] / 16.0, 1)) + + @temperature_thresholds.setter + def temperature_thresholds(self, val: Tuple[float, float]) -> None: + int_low = int(val[0] * 16) + int_high = int(val[1] * 16) + + self._write_u8(_MAX31856_LTHFTH_REG, int_high >> 8) + self._write_u8(_MAX31856_LTHFTL_REG, int_high) + + self._write_u8(_MAX31856_LTLFTH_REG, int_low >> 8) + self._write_u8(_MAX31856_LTLFTL_REG, int_low) + + @property + def reference_temperature_thresholds( # pylint: disable=invalid-name, + self, + ) -> Tuple[float, float]: + """The cold junction's low and high temperature thresholds + as a ``(low_temp, high_temp)`` tuple + """ + return ( + float(unpack("b", self._read_register(_MAX31856_CJLF_REG, 1))[0]), + float(unpack("b", self._read_register(_MAX31856_CJHF_REG, 1))[0]), + ) + + @reference_temperature_thresholds.setter + def reference_temperature_thresholds( # pylint: disable=invalid-name, + self, val: Tuple[float, float] + ) -> None: + self._write_u8(_MAX31856_CJLF_REG, int(val[0])) + self._write_u8(_MAX31856_CJHF_REG, int(val[1])) + + @property + def fault(self) -> Dict[str, bool]: + """A dictionary with the status of each fault type where the key is the fault type and the + value is a bool if the fault is currently active + + =================== ================================= + Key Fault type + =================== ================================= + "cj_range" Cold junction range fault + "tc_range" Thermocouple range fault + "cj_high" Cold junction high threshold fault + "cj_low" Cold junction low threshold fault + "tc_high" Thermocouple high threshold fault + "tc_low" Thermocouple low threshold fault + "voltage" Over/under voltage fault + "open_tc" Thermocouple open circuit fault + =================== ================================= + + """ + faults = self._read_register(_MAX31856_SR_REG, 1)[0] + + return { + "cj_range": bool(faults & _MAX31856_FAULT_CJRANGE), + "tc_range": bool(faults & _MAX31856_FAULT_TCRANGE), + "cj_high": bool(faults & _MAX31856_FAULT_CJHIGH), + "cj_low": bool(faults & _MAX31856_FAULT_CJLOW), + "tc_high": bool(faults & _MAX31856_FAULT_TCHIGH), + "tc_low": bool(faults & _MAX31856_FAULT_TCLOW), + "voltage": bool(faults & _MAX31856_FAULT_OVUV), + "open_tc": bool(faults & _MAX31856_FAULT_OPEN), + } + + def _perform_one_shot_measurement(self) -> None: + self.initiate_one_shot_measurement() + # wait for the measurement to complete + self._wait_for_oneshot() + + def initiate_one_shot_measurement(self) -> None: + """Starts a one-shot measurement and returns immediately. + A measurement takes approximately 160ms. + Check the status of the measurement with `oneshot_pending`; when it is false, + the measurement is complete and the value can be read with `unpack_temperature`. + """ + # read the current value of the first config register + conf_reg_0 = self._read_register(_MAX31856_CR0_REG, 1)[0] + + # and the complement to guarantee the autoconvert bit is unset + conf_reg_0 &= ~_MAX31856_CR0_AUTOCONVERT + # or the oneshot bit to ensure it is set + conf_reg_0 |= _MAX31856_CR0_1SHOT + + # write it back with the new values, prompting the sensor to perform a measurement + self._write_u8(_MAX31856_CR0_REG, conf_reg_0) + + def start_autoconverting(self) -> None: # pylint: disable=no-self-use + """Starts autoconverting temperature measurements. + The sensor will perform a measurement every ~100ms. + """ + # read the current value of the first config register + conf_reg_0 = self._read_register(_MAX31856_CR0_REG, 1)[0] + + # and the complement to guarantee the oneshot bit is unset + conf_reg_0 &= ~_MAX31856_CR0_1SHOT + # or the autoconvert bit to ensure it is set + conf_reg_0 |= _MAX31856_CR0_AUTOCONVERT + + # write it back with the new values, prompting the sensor to perform a measurement + self._write_u8(_MAX31856_CR0_REG, conf_reg_0) + + @property + def oneshot_pending(self) -> bool: + """A boolean indicating the status of the one-shot flag. + A True value means the measurement is still ongoing. + A False value means measurement is complete.""" + oneshot_flag = ( + self._read_register(_MAX31856_CR0_REG, 1)[0] & _MAX31856_CR0_1SHOT + ) + return bool(oneshot_flag) + + def _wait_for_oneshot(self) -> None: + while self.oneshot_pending: + sleep(0.01) + + def _read_register(self, address: int, length: int) -> bytearray: + # pylint: disable=no-member + # Read a 16-bit BE unsigned value from the specified 8-bit address. + with self._device as device: + self._BUFFER[0] = address & 0x7F + device.write(self._BUFFER, end=1) + device.readinto(self._BUFFER, end=length) + return self._BUFFER[:length] + + def _read_sequential_registers(self, start_addr, num_registers=3) -> bytearray: + """ + Read a sequence of `num_registers` registers, starting from `start_addr`. + """ + assert num_registers >= 1, "Number of registers to read must be at least 1" + buf = bytearray(num_registers) + with self._device as device: + # Send read command and start address + device.write(bytearray([start_addr & 0x7F])) + # Read the specified number of registers into the buffer + device.readinto(buf) + return buf + + def _write_u8(self, address: int, val: int) -> None: + # Write an 8-bit unsigned value to the specified 8-bit address. + with self._device as device: + self._BUFFER[0] = (address | 0x80) & 0xFF + self._BUFFER[1] = val & 0xFF + device.write(self._BUFFER, end=2) # pylint: disable=no-member diff --git a/Batt_Board/lib/adafruit_mcp2515/__init__.py b/Batt_Board/lib/adafruit_mcp2515/__init__.py index b6a54ed0..45490f6c 100644 --- a/Batt_Board/lib/adafruit_mcp2515/__init__.py +++ b/Batt_Board/lib/adafruit_mcp2515/__init__.py @@ -384,6 +384,10 @@ def initialize(self): self._set_mode(new_mode) + def sleep(self): + """Put the MCP2515 to sleep""" + self._set_mode(_MODE_SLEEP) + def send(self, message_obj): """Send a message on the bus with the given data and id. If the message could not be sent due to a full fifo or a bus error condition, RuntimeError is raised. diff --git a/Batt_Board/lib/adafruit_register/__init__.py b/Batt_Board/lib/adafruit_register/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Batt_Board/lib/adafruit_register/i2c_bcd_alarm.mpy b/Batt_Board/lib/adafruit_register/i2c_bcd_alarm.mpy new file mode 100644 index 0000000000000000000000000000000000000000..882b011cd50c5d123ea1b7232c5f6ab8f898ec49 GIT binary patch literal 1740 zcmZ8fOLNm!6uwGgV^R}tWFri5LC6n`No+7CB<+I4mcyfY*>N1qzzA8siA7_{m1NR% zQ6v){Wz$7xy6!)yNkZDN>yQ9p+eJx-wkx{pFin^a%U)S=X4)Bz?m6H6&bjBD?q#qTtO&u{4B4OBb9lu-+4OHA35Cz9GX~00FG5OQI_~w z;Kpv+ z0)D+Rua#tpvmzqJ28Am&I4XcD5@e}3I}c!4l&TuSma`;R6@0cAdC_#|5c(9K9c5Y4 zO4tY`lI)<_Ko*yr*=Y?xv$0q~luS#MV%P&ds>G|D^@IwXh3fP)F3f8tEb{Z5TH+N{ z1c!HhjaM|#!ErN4<2c|pr)AX(f^!OPozqH4!qeo7qNK^7L#}EZl8OL|5~ntTDOFRd zg2pwbi8HBDCoea*=EC-!^Jd=vT4*VQ5=T{XpR+E0Xh6&MaC92 zlu4y0Q=`c|mjxrI28RbTBV2N9WPEHiHJatvOA~{M;S>wVT^^{VqCID8F1on@9A7Ld zNL7JB*yX}a+(F?c76t)hO17OZX<9}7C>os+wNkYZ5#(~TxkAyw<}M{IRmUw=o8yn* z7|?FF?P-c+v+u}evsp6Q>^rjB9J`v^9J`|1{5gKMis*7x)#w7ERrFaENdltfX?hk( zGg^t(=44IT zP=PH}$Ootsdb+EXYQ5O6!!~Gx_Hb+09&a1f=Cx5aubr~zb?68aU3S(Ffnn4pbp1cC>m#6aQt+aU=GC2P@Y5j6yx*z{QgkTe|LR}Wgi=Vt}mL$ zoAt%Iha%&T<3!za@KN1$P|q#ZJ)Y&d=TKVs_R9C2xx=xuy?QB_V;CmTlk!CvrpGtR zM1A87GZy!yeQMk{9t;JinFRgF)pg;(( z!HmAMn{i?{+Qb^~Zmgz%|CznGTHC(={$*nPiRleJscmf}*n2OH4YJjJl=+%BR)73q z<;m*p@ZMfxIou+A(>F6f$IeFd?@nBZ#QcHdL!t2Wr^Y^jGYJ>5h`_MSar6;Pt?w>ut0KW_2CR;V9ebu@Q-~n#M2}U3I%dE1tl}lv*b7P`+FnZMt?%ZTE a)@{&AYVrrn{o0qufA?|B?j;&~CHxCXi#~Ax literal 0 HcmV?d00001 diff --git a/Batt_Board/lib/adafruit_register/i2c_bcd_alarm.py b/Batt_Board/lib/adafruit_register/i2c_bcd_alarm.py new file mode 100644 index 00000000..4f18cbcd --- /dev/null +++ b/Batt_Board/lib/adafruit_register/i2c_bcd_alarm.py @@ -0,0 +1,198 @@ +# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT +# pylint: disable=too-few-public-methods + +""" +`adafruit_register.i2c_bcd_alarm` +==================================================== + +Binary Coded Decimal alarm register + +* Author(s): Scott Shawcroft +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Register.git" + +import time + +try: + from typing import Optional, Type, Tuple + from typing_extensions import Literal + from circuitpython_typing.device_drivers import I2CDeviceDriver + + FREQUENCY_T = Literal[ + "monthly", "weekly", "daily", "hourly", "secondly", "minutely" + ] +except ImportError: + pass + + +def _bcd2bin(value: int) -> int: + """Convert binary coded decimal to Binary + + :param value: the BCD value to convert to binary (required, no default) + """ + return value - 6 * (value >> 4) + + +def _bin2bcd(value: int) -> int: + """Convert a binary value to binary coded decimal. + + :param value: the binary value to convert to BCD. (required, no default) + """ + return value + 6 * (value // 10) + + +ALARM_COMPONENT_DISABLED = 0x80 +FREQUENCY = ["secondly", "minutely", "hourly", "daily", "weekly", "monthly"] + + +class BCDAlarmTimeRegister: + """ + Alarm date and time register using binary coded decimal structure. + + The byte order of the registers must* be: [second], minute, hour, day, + weekday. Each byte must also have a high enable bit where 1 is disabled and + 0 is enabled. + + * If weekday_shared is True, then weekday and day share a register. + * If has_seconds is True, then there is a seconds register. + + Values are a tuple of (`time.struct_time`, `str`) where the struct represents + a date and time that would alarm. The string is the frequency: + + * "secondly", once a second (only if alarm has_seconds) + * "minutely", once a minute when seconds match (if alarm doesn't seconds then when seconds = 0) + * "hourly", once an hour when ``tm_min`` and ``tm_sec`` match + * "daily", once a day when ``tm_hour``, ``tm_min`` and ``tm_sec`` match + * "weekly", once a week when ``tm_wday``, ``tm_hour``, ``tm_min``, ``tm_sec`` match + * "monthly", once a month when ``tm_mday``, ``tm_hour``, ``tm_min``, ``tm_sec`` match + + :param int register_address: The register address to start the read + :param bool has_seconds: True if the alarm can happen minutely. + :param bool weekday_shared: True if weekday and day share the same register + :param int weekday_start: 0 or 1 depending on the RTC's representation of the first day of the + week (Monday) + """ + + # Defaults are based on alarm1 of the DS3231. + def __init__( + self, + register_address: int, + has_seconds: bool = True, + weekday_shared: bool = True, + weekday_start: Literal[0, 1] = 1, + ) -> None: + buffer_size = 5 + if weekday_shared: + buffer_size -= 1 + if has_seconds: + buffer_size += 1 + self.has_seconds = has_seconds + self.buffer = bytearray(buffer_size) + self.buffer[0] = register_address + self.weekday_shared = weekday_shared + self.weekday_start = weekday_start + + def __get__( + self, + obj: Optional[I2CDeviceDriver], + objtype: Optional[Type[I2CDeviceDriver]] = None, + ) -> Tuple[time.struct_time, FREQUENCY_T]: + # Read the alarm register. + with obj.i2c_device as i2c: + i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1) + + frequency = None + i = 1 + seconds = 0 + if self.has_seconds: + if (self.buffer[1] & 0x80) != 0: + frequency = "secondly" + else: + frequency = "minutely" + seconds = _bcd2bin(self.buffer[1] & 0x7F) + i = 2 + minute = 0 + if (self.buffer[i] & 0x80) == 0: + frequency = "hourly" + minute = _bcd2bin(self.buffer[i] & 0x7F) + + hour = 0 + if (self.buffer[i + 1] & 0x80) == 0: + frequency = "daily" + hour = _bcd2bin(self.buffer[i + 1] & 0x7F) + + mday = None + wday = None + if (self.buffer[i + 2] & 0x80) == 0: + # day of the month + if not self.weekday_shared or (self.buffer[i + 2] & 0x40) == 0: + frequency = "monthly" + mday = _bcd2bin(self.buffer[i + 2] & 0x3F) + else: # weekday + frequency = "weekly" + wday = _bcd2bin(self.buffer[i + 2] & 0x3F) - self.weekday_start + + # weekday + if not self.weekday_shared and (self.buffer[i + 3] & 0x80) == 0: + frequency = "monthly" + mday = _bcd2bin(self.buffer[i + 3] & 0x7F) + + if mday is not None: + wday = (mday - 2) % 7 + elif wday is not None: + mday = wday + 2 + else: + # Jan 1, 2017 was a Sunday (6) + wday = 6 + mday = 1 + + return ( + time.struct_time((2017, 1, mday, hour, minute, seconds, wday, mday, -1)), + frequency, + ) + + def __set__( + self, obj: I2CDeviceDriver, value: Tuple[time.struct_time, FREQUENCY_T] + ) -> None: + if len(value) != 2: + raise ValueError("Value must be sequence of length two") + # Turn all components off by default. + for i in range(len(self.buffer) - 1): + self.buffer[i + 1] = ALARM_COMPONENT_DISABLED + frequency_name = value[1] + error_message = "%s is not a supported frequency" % frequency_name + if frequency_name not in FREQUENCY: + raise ValueError(error_message) + + frequency = FREQUENCY.index(frequency_name) + if frequency <= 1 and not self.has_seconds: + raise ValueError(error_message) + + # i is the index of the minute byte + i = 2 if self.has_seconds else 1 + + if frequency > 0 and self.has_seconds: # minutely at least + self.buffer[1] = _bin2bcd(value[0].tm_sec) + + if frequency > 1: # hourly at least + self.buffer[i] = _bin2bcd(value[0].tm_min) + + if frequency > 2: # daily at least + self.buffer[i + 1] = _bin2bcd(value[0].tm_hour) + + if value[1] == "weekly": + if self.weekday_shared: + self.buffer[i + 2] = ( + _bin2bcd(value[0].tm_wday + self.weekday_start) | 0x40 + ) + else: + self.buffer[i + 3] = _bin2bcd(value[0].tm_wday + self.weekday_start) + elif value[1] == "monthly": + self.buffer[i + 2] = _bin2bcd(value[0].tm_mday) + + with obj.i2c_device: + obj.i2c_device.write(self.buffer) diff --git a/Batt_Board/lib/adafruit_register/i2c_bcd_datetime.mpy b/Batt_Board/lib/adafruit_register/i2c_bcd_datetime.mpy new file mode 100644 index 0000000000000000000000000000000000000000..4ca5d752bf25244f7b933f3fd4797de2f412ae50 GIT binary patch literal 1141 zcmYLG+j81g6g|Q>h%pF6R9~p=0H!v@$ic2B?Tgz$-Nw^SgKr&XnmG~D0hAPpkq&Wq zLc)Z&?L&V>Nj@PT(oe|L&h*vY=TPX3Mp|oWU)L_pqBSLOr0Qzj?idu?q+vLe*oslq za81*(u2Mpc7Ads5C>p+B__A>S0SYn&i9fZeVVUZ$D0td!6BMT1wqZ6<Z?4!`7g4yGxsviANa)orhqZ$2chauv9L?Ag9pw z_`eB}$mT2743i-RyArvi-xkFVOXiIgTPj*8Qj2nbv zj1p|^QFD~Nuwf5Oo5TcLRNXMCg(6mmVq)q@G)z3IB|FsaXcTi#pc1r$N@3apr15VH zW{d!DS{<8%3z&?976UA_WsO3*M72@>k&H2z!z_bRB8L8L3($}KZfz@`_&EGp~w?vt=cn~w)PAhC1~cLNom{pN>LgHZFXt}&1xy*qbuKxuU{J7;_>Jf zW2Xy{gJy%bz23pW+}bS~>iLxL^lLYiDIcba`7>=090bLHoJu1(?N(N%?!JGQxC_bf zh_WYs;_VLt)ygkV;!}ymco@{zXTB7FefD8bNd5WvnoC0_4VlF0D_;_Cm^7G3myBLc zXrIykgnEp66FPwWYbL!q``}Bln?0e|%Z2s@?ds*h>eANshTGW5=TpZ!`JHTh;jOd` zqqsc?je6&duYdzmw9j#p<4cY|g<{v7^A22F<(yB<`5Na#j@My9XwG&%XVxrK;hne# Ld|zOeRJ4Bq)KgmQ literal 0 HcmV?d00001 diff --git a/Batt_Board/lib/adafruit_register/i2c_bcd_datetime.py b/Batt_Board/lib/adafruit_register/i2c_bcd_datetime.py new file mode 100644 index 00000000..2bb458ad --- /dev/null +++ b/Batt_Board/lib/adafruit_register/i2c_bcd_datetime.py @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT +# pylint: disable=too-few-public-methods + +""" +`adafruit_register.i2c_bcd_datetime` +==================================================== + +Binary Coded Decimal date and time register + +* Author(s): Scott Shawcroft +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Register.git" + +import time + +try: + from typing import Optional, Type + from typing_extensions import Literal + from circuitpython_typing.device_drivers import I2CDeviceDriver +except ImportError: + pass + + +def _bcd2bin(value: int) -> int: + """Convert binary coded decimal to Binary + + :param value: the BCD value to convert to binary (required, no default) + """ + return value - 6 * (value >> 4) + + +def _bin2bcd(value: int) -> int: + """Convert a binary value to binary coded decimal. + + :param value: the binary value to convert to BCD. (required, no default) + """ + return value + 6 * (value // 10) + + +class BCDDateTimeRegister: + """ + Date and time register using binary coded decimal structure. + + The byte order of the register must* be: second, minute, hour, weekday, day (1-31), month, year + (in years after 2000). + + * Setting weekday_first=False will flip the weekday/day order so that day comes first. + + Values are `time.struct_time` + + :param int register_address: The register address to start the read + :param bool weekday_first: True if weekday is in a lower register than the day of the month + (1-31) + :param int weekday_start: 0 or 1 depending on the RTC's representation of the first day of the + week + """ + + def __init__( + self, + register_address: int, + weekday_first: bool = True, + weekday_start: Literal[0, 1] = 1, + ) -> None: + self.buffer = bytearray(8) + self.buffer[0] = register_address + if weekday_first: + self.weekday_offset = 0 + else: + self.weekday_offset = 1 + self.weekday_start = weekday_start + # Masking value list n/a sec min hr day wkday mon year + self.mask_datetime = b"\xFF\x7F\x7F\x3F\x3F\x07\x1F\xFF" + + def __get__( + self, + obj: Optional[I2CDeviceDriver], + objtype: Optional[Type[I2CDeviceDriver]] = None, + ) -> time.struct_time: + # Read and return the date and time. + with obj.i2c_device as i2c: + i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1) + return time.struct_time( + ( + _bcd2bin(self.buffer[7] & self.mask_datetime[7]) + 2000, + _bcd2bin(self.buffer[6] & self.mask_datetime[6]), + _bcd2bin(self.buffer[5 - self.weekday_offset] & self.mask_datetime[4]), + _bcd2bin(self.buffer[3] & self.mask_datetime[3]), + _bcd2bin(self.buffer[2] & self.mask_datetime[2]), + _bcd2bin(self.buffer[1] & self.mask_datetime[1]), + _bcd2bin( + (self.buffer[4 + self.weekday_offset] & self.mask_datetime[5]) + - self.weekday_start + ), + -1, + -1, + ) + ) + + def __set__(self, obj: I2CDeviceDriver, value: time.struct_time) -> None: + self.buffer[1] = _bin2bcd(value.tm_sec) & 0x7F # format conversions + self.buffer[2] = _bin2bcd(value.tm_min) + self.buffer[3] = _bin2bcd(value.tm_hour) + self.buffer[4 + self.weekday_offset] = _bin2bcd( + value.tm_wday + self.weekday_start + ) + self.buffer[5 - self.weekday_offset] = _bin2bcd(value.tm_mday) + self.buffer[6] = _bin2bcd(value.tm_mon) + self.buffer[7] = _bin2bcd(value.tm_year - 2000) + with obj.i2c_device: + obj.i2c_device.write(self.buffer) diff --git a/Batt_Board/lib/adafruit_register/i2c_bit.mpy b/Batt_Board/lib/adafruit_register/i2c_bit.mpy new file mode 100644 index 0000000000000000000000000000000000000000..53762b9ba6e99bd7564a87edd81f375bbdd7644f GIT binary patch literal 801 zcmZvY(M}pc6ozL(5LSc{aEh{3E0#9IvK6VpUbHH-RgzXg)Dml^u)DxmWy$WKA!(Cc zstLXgN^g4IH|Ptr=?mD{r0Hb`s7)`LOmfbgobNy9{7E~Qyu&35a-pQv3B{UHCOTC# zk;EiiBs5xU0FP@fx)_~H0JvVGM6DJMfFswaDFD7uvkI+iRp6qH8mX4S#9AzwQjUnE zq%?A*XyBHk<8nmKTalb7p_gb*r>(K$9-B}@&h4pR-K{gj37W6m3RjijvOvO=b zPzAU#E-Msc;O{Qi*8t&TjZg*Cic)3E7i3bUDsZcHij}GiAgN-V7Bt$0^&Z?@Lm0Ew zban~Tf|y}7m1WPxdk>nvscQ#tL6$W|*MXg_1BAcjVcyy}9*Wl{i_6z;C;R4X^v%is@jl8JaQb$3oO``w zIA>DRi7n|&gcgKC0Nq#(8_)^|pepPs=$nhQ^^214vN;dXP$t4{ z&U45Uvmk#)vYvpCC*1guaC2gE%9yCp$)N%F8vR^NNx1NmtFGf?)J+1KmX6)cDFnJQC3;t ix>GWeISY8cwQBIa+j$g>f$phXCFtsn+w_LxK=} None: + self.bit_mask = 1 << (bit % 8) # the bitmask *within* the byte! + self.buffer = bytearray(1 + register_width) + self.buffer[0] = register_address + if lsb_first: + self.byte = bit // 8 + 1 # the byte number within the buffer + else: + self.byte = register_width - (bit // 8) # the byte number within the buffer + + def __get__( + self, + obj: Optional[I2CDeviceDriver], + objtype: Optional[Type[I2CDeviceDriver]] = None, + ) -> bool: + with obj.i2c_device as i2c: + i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1) + return bool(self.buffer[self.byte] & self.bit_mask) + + def __set__(self, obj: I2CDeviceDriver, value: bool) -> None: + with obj.i2c_device as i2c: + i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1) + if value: + self.buffer[self.byte] |= self.bit_mask + else: + self.buffer[self.byte] &= ~self.bit_mask + i2c.write(self.buffer) + + +class ROBit(RWBit): + """Single bit register that is read only. Subclass of `RWBit`. + + Values are `bool` + + :param int register_address: The register address to read the bit from + :param type bit: The bit index within the byte at ``register_address`` + :param int register_width: The number of bytes in the register. Defaults to 1. + + """ + + def __set__(self, obj: I2CDeviceDriver, value: bool) -> NoReturn: + raise AttributeError() diff --git a/Batt_Board/lib/adafruit_register/i2c_bits.mpy b/Batt_Board/lib/adafruit_register/i2c_bits.mpy new file mode 100644 index 0000000000000000000000000000000000000000..a9846d634f5635478949327a402bf6a65ec0c381 GIT binary patch literal 1053 zcmZvZ-%}G;6vyu-1Weo{H=DSP5-|pnV1%TEtz&(ukSGXM2_g(LI=7qMOLi^nrn@&V z>2${3Oc`STnnePCfJXm-_R)uS+D_j(#^N8)n?-zL=FXgZ@A>iloO4Qp;L3#OmMDuG zMnfgYz%|t**eIw)2~|~M=Ic!mW~cIB=cjH0_^?h?T@yFKxs_%e1NcB+#-w3rfF;ek zs@1^Ay<(}1w^a$34RsqE;I5<^60KZs5=Gb0NmyQXLy+von1Eego26RRK6LFUq|#_h zG`|P2O??NOq|XA_YGY%A*5o$LD%wyDlK^O{HO+N~5UOE<5Qz2BPXs__$507IM8O(u zO_WuQ=pdvw2*R2SpsFF0hz0>P*>saz_jrUTw@KTiwIHHtb)DYs7bd@IMWNQ%a>oLa zr;|dWEF0J~LE`hzjw%xcuzfTv1Knl@_3AS!TYW~`$G~pdYtX=b73yx?nelz25K=d9 z6$&+#D2-}f(zgnyD=EyJ?xu7y$)%G?E`Qoop5_4GcuCYWog@`;8z;AP11IUg%p_4n z?K5f8RDZ-^_osXCy?{EFIwSW zqU~5@m`^}If7xDM%6T761O9v^$2^*5_(;*i^A*XvJOA4*lR7_gB^kULPFYh6*Rx}} z%$U93vyTsYPMme3tlepU?7b_`aigP*z~BQX9^}UC&VK^q#6vHgXvkVUbmHNo@|?YI zANOWwEGG^t(%$@DBo!G+XRONqJJJhf503LA>72Ec$xKu2$@I6`^0i!s(gy!})o0i_i{Wg5`&`!kge7>+ z=J@r`Zs+A6U6&LsM&5Q(_H)V>I$G_t|L)FC9>l(&uNzFi%Zg-AfIy%vTdcdD0Drj% R`paOHVb+EIfx9jM{6D?nP#gdN literal 0 HcmV?d00001 diff --git a/Batt_Board/lib/adafruit_register/i2c_bits.py b/Batt_Board/lib/adafruit_register/i2c_bits.py new file mode 100644 index 00000000..9a9f1d2d --- /dev/null +++ b/Batt_Board/lib/adafruit_register/i2c_bits.py @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT +# pylint: disable=too-few-public-methods + +""" +`adafruit_register.i2c_bits` +==================================================== + +Multi bit registers + +* Author(s): Scott Shawcroft +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Register.git" + +try: + from typing import Optional, Type, NoReturn + from circuitpython_typing.device_drivers import I2CDeviceDriver +except ImportError: + pass + + +class RWBits: + """ + Multibit register (less than a full byte) that is readable and writeable. + This must be within a byte register. + + Values are `int` between 0 and 2 ** ``num_bits`` - 1. + + :param int num_bits: The number of bits in the field. + :param int register_address: The register address to read the bit from + :param int lowest_bit: The lowest bits index within the byte at ``register_address`` + :param int register_width: The number of bytes in the register. Defaults to 1. + :param bool lsb_first: Is the first byte we read from I2C the LSB? Defaults to true + :param bool signed: If True, the value is a "two's complement" signed value. + If False, it is unsigned. + """ + + def __init__( # pylint: disable=too-many-arguments + self, + num_bits: int, + register_address: int, + lowest_bit: int, + register_width: int = 1, + lsb_first: bool = True, + signed: bool = False, + ) -> None: + self.bit_mask = ((1 << num_bits) - 1) << lowest_bit + # print("bitmask: ",hex(self.bit_mask)) + if self.bit_mask >= 1 << (register_width * 8): + raise ValueError("Cannot have more bits than register size") + self.lowest_bit = lowest_bit + self.buffer = bytearray(1 + register_width) + self.buffer[0] = register_address + self.lsb_first = lsb_first + self.sign_bit = (1 << (num_bits - 1)) if signed else 0 + + def __get__( + self, + obj: Optional[I2CDeviceDriver], + objtype: Optional[Type[I2CDeviceDriver]] = None, + ) -> int: + with obj.i2c_device as i2c: + i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1) + # read the number of bytes into a single variable + reg = 0 + order = range(len(self.buffer) - 1, 0, -1) + if not self.lsb_first: + order = reversed(order) + for i in order: + reg = (reg << 8) | self.buffer[i] + reg = (reg & self.bit_mask) >> self.lowest_bit + # If the value is signed and negative, convert it + if reg & self.sign_bit: + reg -= 2 * self.sign_bit + return reg + + def __set__(self, obj: I2CDeviceDriver, value: int) -> None: + value <<= self.lowest_bit # shift the value over to the right spot + with obj.i2c_device as i2c: + i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1) + reg = 0 + order = range(len(self.buffer) - 1, 0, -1) + if not self.lsb_first: + order = range(1, len(self.buffer)) + for i in order: + reg = (reg << 8) | self.buffer[i] + # print("old reg: ", hex(reg)) + reg &= ~self.bit_mask # mask off the bits we're about to change + reg |= value # then or in our new value + # print("new reg: ", hex(reg)) + for i in reversed(order): + self.buffer[i] = reg & 0xFF + reg >>= 8 + i2c.write(self.buffer) + + +class ROBits(RWBits): + """ + Multibit register (less than a full byte) that is read-only. This must be + within a byte register. + + Values are `int` between 0 and 2 ** ``num_bits`` - 1. + + :param int num_bits: The number of bits in the field. + :param int register_address: The register address to read the bit from + :param type lowest_bit: The lowest bits index within the byte at ``register_address`` + :param int register_width: The number of bytes in the register. Defaults to 1. + """ + + def __set__(self, obj: I2CDeviceDriver, value: int) -> NoReturn: + raise AttributeError() diff --git a/Batt_Board/lib/adafruit_register/i2c_struct.mpy b/Batt_Board/lib/adafruit_register/i2c_struct.mpy new file mode 100644 index 0000000000000000000000000000000000000000..21b38000c7d98d73c16da70e03ab7958c0c1f144 GIT binary patch literal 1040 zcmZvZ-A)=o7=~v-5OKlb$0-U`E4o&Tpj%2?o3t9ys)@BIYDhHOWf@?t>~3}k4L#bW zCdIo!=^fbn=p{7iNl!a0k~EFU9L&%Az2En|v+f3y3FfU>63cpBCW4MDvO%z(l6grm zh+daSQfq(_b~*Vvx%>*Sy*mK+G$N~tcn&=IhK7MRU)RntaIYy106(bvnACLzu%w~M zN(D@8^XplBAxk)`%NJM&o06RaFssvSi7Y2qjx%0QSJ{5740^>W%xu`2OC% z$G9~QCGlJ`B`B6D*J_Rw$)dk`|ajV5MIz#b{27p1hhHI+cxRCKBXl|^z-cK-_mceBOLG7I|B|!@d?Sfz=F_SM(;y1Laja3nr6YjQLEZ94*koE3D21MLvZFu&7GI}*HUhl1u&PWoE?#lTQ} z?x~sLGB=^{`_a2jc$7ZU1(5|GS(z(GA$M|0>m#&2I8PtajFn3|4l@p^sU5?y_}qe- zUgD@?oGR$Mb=!J5Q0Tf1Q@1F+$uM1yoYV#IwN@y=RBPA<@I44%MdE%NU}L5`|4lm* z%Sl&Tf0(d6>+TZA@&7^HwL`IvdDQJ1`0+hF+UYzrf5*x=%|q$Ra~!tk&M$F`+`~@y pJRR#c(}NkJw$V->#YzCLzjb1IZ4AGSL9=J7QPy^@1fQh={tHqnAt(R< literal 0 HcmV?d00001 diff --git a/Batt_Board/lib/adafruit_register/i2c_struct.py b/Batt_Board/lib/adafruit_register/i2c_struct.py new file mode 100644 index 00000000..2f546a8f --- /dev/null +++ b/Batt_Board/lib/adafruit_register/i2c_struct.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT +# pylint: disable=too-few-public-methods + +""" +`adafruit_register.i2c_struct` +==================================================== + +Generic structured registers based on `struct` + +* Author(s): Scott Shawcroft +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Register.git" + +import struct + +try: + from typing import Optional, Type, Tuple, Any, NoReturn + from circuitpython_typing.device_drivers import I2CDeviceDriver +except ImportError: + pass + + +class Struct: + """ + Arbitrary structure register that is readable and writeable. + + Values are tuples that map to the values in the defined struct. See struct + module documentation for struct format string and its possible value types. + + :param int register_address: The register address to read the bit from + :param str struct_format: The struct format string for this register. + """ + + def __init__(self, register_address: int, struct_format: str) -> None: + self.format = struct_format + self.buffer = bytearray(1 + struct.calcsize(self.format)) + self.buffer[0] = register_address + + def __get__( + self, + obj: Optional[I2CDeviceDriver], + objtype: Optional[Type[I2CDeviceDriver]] = None, + ) -> Tuple: + with obj.i2c_device as i2c: + i2c.write_then_readinto(self.buffer, self.buffer, out_end=1, in_start=1) + return struct.unpack_from(self.format, memoryview(self.buffer)[1:]) + + def __set__(self, obj: I2CDeviceDriver, value: Tuple) -> None: + struct.pack_into(self.format, self.buffer, 1, *value) + with obj.i2c_device as i2c: + i2c.write(self.buffer) + + +class UnaryStruct: + """ + Arbitrary single value structure register that is readable and writeable. + + Values map to the first value in the defined struct. See struct + module documentation for struct format string and its possible value types. + + :param int register_address: The register address to read the bit from + :param str struct_format: The struct format string for this register. + """ + + def __init__(self, register_address: int, struct_format: str) -> None: + self.format = struct_format + self.address = register_address + + def __get__( + self, + obj: Optional[I2CDeviceDriver], + objtype: Optional[Type[I2CDeviceDriver]] = None, + ) -> Any: + buf = bytearray(1 + struct.calcsize(self.format)) + buf[0] = self.address + with obj.i2c_device as i2c: + i2c.write_then_readinto(buf, buf, out_end=1, in_start=1) + return struct.unpack_from(self.format, buf, 1)[0] + + def __set__(self, obj: I2CDeviceDriver, value: Any) -> None: + buf = bytearray(1 + struct.calcsize(self.format)) + buf[0] = self.address + struct.pack_into(self.format, buf, 1, value) + with obj.i2c_device as i2c: + i2c.write(buf) + + +class ROUnaryStruct(UnaryStruct): + """ + Arbitrary single value structure register that is read-only. + + Values map to the first value in the defined struct. See struct + module documentation for struct format string and its possible value types. + + :param int register_address: The register address to read the bit from + :param type struct_format: The struct format string for this register. + """ + + def __set__(self, obj: I2CDeviceDriver, value: Any) -> NoReturn: + raise AttributeError() diff --git a/Batt_Board/lib/adafruit_register/i2c_struct_array.mpy b/Batt_Board/lib/adafruit_register/i2c_struct_array.mpy new file mode 100644 index 0000000000000000000000000000000000000000..8871367e98a10a4d912ad99c58c24a991646b91a GIT binary patch literal 1008 zcmYk3ZEw?76vwagM4p=5*yb)Yp)E~F(*Vh0veF`?jiwZ+HVs3e%C_p(y>?ndYg_gW zD0`6XP)*&paS}cO-8a~aNxa#2Kzsmh;#T90b*%Hh=YRgc(~g1pW@t-Mm9E{>DYmr( z-JzP@&?SN$YWE1mift?Xy445SbhExzZ?1xL&;iNqo^_-F_@hO2(@>5;e7kRHAWi$0 zZXAHQ$5MMkd#w{~!`5GGHrOP(O_+_|B)HZ{LL@doJ2 zpOUv+?CQ4j|FR%v?!N@yWrPnjiuZfnF7tv!IUH>V zb%UB9WA-T43>84#V5t?G0@O1sg}lOD+dKwq{Is5G2alICimKY0;{Z5CgLM^T@YEXv zgfUC%FdP^Ih?$3Fvg(J%t#1ax!UbM@F=6(cao4nu6$-pH+2aNeGAs{+qXF;spx`y4 z-yTwGId>b41DziB_UptvZd{zKasT3W?EvnR0PfTCOV-&N5Y3z)bGq;PJiIen6O5IcUawSxTNpxKT zC@pa)Cxwt8h0zr$f{HSU4!|&i(v|E|6}mUdI`%0_Lw& zmT$W2OZ95-@{ILoS%2(}PVYoI!JNe3@5I7*g`2#W$&*C9SaBb*6^ZwIJd-;8-i7BN zQUM6L#yz{uq4^20svi;4l}Y|xmY-#xVcD-T4zR?DGi+BsKPR&Y|8U}bC40+#Rcn`I hzCQGYxmKvX!#5@spJd;wY%ONminPKV{_8+MJ_8bIDg^)l literal 0 HcmV?d00001 diff --git a/Batt_Board/lib/adafruit_register/i2c_struct_array.py b/Batt_Board/lib/adafruit_register/i2c_struct_array.py new file mode 100644 index 00000000..02d373c8 --- /dev/null +++ b/Batt_Board/lib/adafruit_register/i2c_struct_array.py @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT +# pylint: disable=too-few-public-methods + +""" +`adafruit_register.i2c_struct_array` +==================================================== + +Array of structured registers based on `struct` + +* Author(s): Scott Shawcroft +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Register.git" + +import struct + +try: + from typing import Tuple, Optional, Type + from circuitpython_typing.device_drivers import I2CDeviceDriver +except ImportError: + pass + + +class _BoundStructArray: + """ + Array object that `StructArray` constructs on demand. + + :param object obj: The device object to bind to. It must have a `i2c_device` attribute + :param int register_address: The register address to read the bit from + :param str struct_format: The struct format string for each register element + :param int count: Number of elements in the array + """ + + def __init__( + self, + obj: I2CDeviceDriver, + register_address: int, + struct_format: str, + count: int, + ) -> None: + self.format = struct_format + self.first_register = register_address + self.obj = obj + self.count = count + + def _get_buffer(self, index: int) -> bytearray: + """Shared bounds checking and buffer creation.""" + if not 0 <= index < self.count: + raise IndexError() + size = struct.calcsize(self.format) + # We create the buffer every time instead of keeping the buffer (which is 32 bytes at least) + # around forever. + buf = bytearray(size + 1) + buf[0] = self.first_register + size * index + return buf + + def __getitem__(self, index: int) -> Tuple: + buf = self._get_buffer(index) + with self.obj.i2c_device as i2c: + i2c.write_then_readinto(buf, buf, out_end=1, in_start=1) + return struct.unpack_from(self.format, buf, 1) # offset=1 + + def __setitem__(self, index: int, value: Tuple) -> None: + buf = self._get_buffer(index) + struct.pack_into(self.format, buf, 1, *value) + with self.obj.i2c_device as i2c: + i2c.write(buf) + + def __len__(self) -> int: + return self.count + + +class StructArray: + """ + Repeated array of structured registers that are readable and writeable. + + Based on the index, values are offset by the size of the structure. + + Values are tuples that map to the values in the defined struct. See struct + module documentation for struct format string and its possible value types. + + .. note:: This assumes the device addresses correspond to 8-bit bytes. This is not suitable for + devices with registers of other widths such as 16-bit. + + :param int register_address: The register address to begin reading the array from + :param str struct_format: The struct format string for this register. + :param int count: Number of elements in the array + """ + + def __init__(self, register_address: int, struct_format: str, count: int) -> None: + self.format = struct_format + self.address = register_address + self.count = count + self.array_id = "_structarray{}".format(register_address) + + def __get__( + self, + obj: Optional[I2CDeviceDriver], + objtype: Optional[Type[I2CDeviceDriver]] = None, + ) -> _BoundStructArray: + # We actually can't handle the indexing ourself due to data descriptor limits. So, we return + # an object that can instead. This object is bound to the object passed in here by its + # initializer and then cached on the object itself. That way its lifetime is tied to the + # lifetime of the object itself. + if not hasattr(obj, self.array_id): + setattr( + obj, + self.array_id, + _BoundStructArray(obj, self.address, self.format, self.count), + ) + return getattr(obj, self.array_id) diff --git a/Batt_Board/lib/adafruit_vl6180x.py b/Batt_Board/lib/adafruit_vl6180x.py new file mode 100644 index 00000000..baf92459 --- /dev/null +++ b/Batt_Board/lib/adafruit_vl6180x.py @@ -0,0 +1,411 @@ +# SPDX-FileCopyrightText: 2017 Tony DiCola for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_vl6180x` +==================================================== + +CircuitPython module for the VL6180X distance sensor. See +examples/simpletest.py for a demo of the usage. + +* Author(s): Tony DiCola, Jonas Schatz + +Implementation Notes +-------------------- + +**Hardware:** + +* Adafruit `VL6180X Time of Flight Distance Ranging Sensor (VL6180) + `_ (Product ID: 3316) + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: + https://github.com/adafruit/circuitpython/releases +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +""" +import struct +import time + +from micropython import const + +from adafruit_bus_device import i2c_device + +try: + from typing import Optional, List + from busio import I2C +except ImportError: + pass + + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_VL6180X.git" + +# Registers +_VL6180X_REG_IDENTIFICATION_MODEL_ID = const(0x000) + +_VL6180X_REG_SYSTEM_HISTORY_CTRL = const(0x012) +_VL6180X_REG_SYSTEM_INTERRUPT_CONFIG = const(0x014) +_VL6180X_REG_SYSTEM_INTERRUPT_CLEAR = const(0x015) +_VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET = const(0x016) + +_VL6180X_REG_SYSRANGE_START = const(0x018) +_VL6180X_REG_SYSRANGE_INTERMEASUREMENT_PERIOD = const(0x01B) +_VL6180X_REG_SYSRANGE_PART_TO_PART_RANGE_OFFSET = const(0x024) + +_VL6180X_REG_SYSALS_START = const(0x038) +_VL6180X_REG_SYSALS_ANALOGUE_GAIN = const(0x03F) +_VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI = const(0x040) +_VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO = const(0x041) + +_VL6180X_REG_RESULT_RANGE_STATUS = const(0x04D) +_VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO = const(0x04F) +_VL6180X_REG_RESULT_ALS_VAL = const(0x050) +_VL6180X_REG_RESULT_HISTORY_BUFFER_0 = const(0x052) +_VL6180X_REG_RESULT_RANGE_VAL = const(0x062) + +# Internal constants: +_VL6180X_DEFAULT_I2C_ADDR = const(0x29) + +# User-facing constants: +ALS_GAIN_1 = const(0x06) +ALS_GAIN_1_25 = const(0x05) +ALS_GAIN_1_67 = const(0x04) +ALS_GAIN_2_5 = const(0x03) +ALS_GAIN_5 = const(0x02) +ALS_GAIN_10 = const(0x01) +ALS_GAIN_20 = const(0x00) +ALS_GAIN_40 = const(0x07) + +ERROR_NONE = const(0) +ERROR_SYSERR_1 = const(1) +ERROR_SYSERR_5 = const(5) +ERROR_ECEFAIL = const(6) +ERROR_NOCONVERGE = const(7) +ERROR_RANGEIGNORE = const(8) +ERROR_SNR = const(11) +ERROR_RAWUFLOW = const(12) +ERROR_RAWOFLOW = const(13) +ERROR_RANGEUFLOW = const(14) +ERROR_RANGEOFLOW = const(15) + + +class VL6180X: + """Create an instance of the VL6180X distance sensor. You must pass in + the following parameters: + + :param ~I2C i2c: An instance of the I2C bus connected to the sensor. + + Optionally you can specify: + + :param int address: The I2C address of the sensor. If not specified the sensor's + default value will be assumed. + :param int offset: The offset to be applied to measurements, in mm + """ + + def __init__( + self, i2c: I2C, address: int = _VL6180X_DEFAULT_I2C_ADDR, offset: int = 0 + ) -> None: + self._device = i2c_device.I2CDevice(i2c, address) + if self._read_8(_VL6180X_REG_IDENTIFICATION_MODEL_ID) != 0xB4: + raise RuntimeError("Could not find VL6180X, is it connected and powered?") + self._load_settings() + self._write_8(_VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET, 0x00) + self.offset = offset + + # Reset a sensor that crashed while in continuous mode + if self.continuous_mode_enabled: + self.stop_range_continuous() + time.sleep(0.1) + + # Activate history buffer for range measurement + self._write_8(_VL6180X_REG_SYSTEM_HISTORY_CTRL, 0x01) + + @property + def range(self) -> int: + """Read the range of an object in front of sensor and return it in mm.""" + if self.continuous_mode_enabled: + return self._read_range_continuous() + return self._read_range_single() + + @property + def range_from_history(self) -> Optional[int]: + """Read the latest range data from history + To do so, you don't have to wait for a complete measurement.""" + + if not self.range_history_enabled: + return None + + return self._read_8(_VL6180X_REG_RESULT_HISTORY_BUFFER_0) + + @property + def ranges_from_history(self) -> Optional[List[int]]: + """Read the last 16 range measurements from history""" + + if not self.range_history_enabled: + return None + + return [ + self._read_8(_VL6180X_REG_RESULT_HISTORY_BUFFER_0 + age) + for age in range(16) + ] + + @property + def range_history_enabled(self) -> bool: + """Checks if history buffer stores range data""" + + history_ctrl: int = self._read_8(_VL6180X_REG_SYSTEM_HISTORY_CTRL) + + if history_ctrl & 0x0: + print("History buffering not enabled") + return False + + if (history_ctrl > 1) & 0x1: + print("History buffer stores ALS data, not range") + return False + + return True + + def start_range_continuous(self, period: int = 100) -> None: + """Start continuous range mode + + :param int period: Time delay between measurements, in milliseconds; the value you + will be floored to the nearest 10 milliseconds (setting to 157 ms sets it to 150 + ms). Range is 10 - 2550 ms. + """ + # Set range between measurements + if not 10 <= period <= 2550: + raise ValueError( + "Delay must be in 10 millisecond increments between 10 and 2550 milliseconds" + ) + + period_reg = (period // 10) - 1 + self._write_8(_VL6180X_REG_SYSRANGE_INTERMEASUREMENT_PERIOD, period_reg) + + # Start continuous range measurement + self._write_8(_VL6180X_REG_SYSRANGE_START, 0x03) + + def stop_range_continuous(self) -> None: + """Stop continuous range mode. It is advised to wait for about 0.3s + afterwards to avoid issues with the interrupt flags""" + if self.continuous_mode_enabled: + self._write_8(_VL6180X_REG_SYSRANGE_START, 0x01) + + @property + def continuous_mode_enabled(self) -> bool: + """Checks if continuous mode is enabled""" + return self._read_8(_VL6180X_REG_SYSRANGE_START) > 1 & 0x1 + + @property + def offset(self) -> int: + """Read and sets the manual offset for the sensor, in millimeters""" + return self._offset + + @offset.setter + def offset(self, offset: int) -> None: + self._write_8( + _VL6180X_REG_SYSRANGE_PART_TO_PART_RANGE_OFFSET, struct.pack("b", offset)[0] + ) + self._offset = offset + + def _read_range_single(self) -> int: + """Read the range when in single-shot mode""" + while not self._read_8(_VL6180X_REG_RESULT_RANGE_STATUS) & 0x01: + pass + self._write_8(_VL6180X_REG_SYSRANGE_START, 0x01) + return self._read_range_continuous() + + def _read_range_continuous(self) -> int: + """Read the range when in continuous mode""" + + # Poll until bit 2 is set + while not self._read_8(_VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) & 0x04: + pass + + # read range in mm + range_ = self._read_8(_VL6180X_REG_RESULT_RANGE_VAL) + + # clear interrupt + self._write_8(_VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07) + + return range_ + + def read_lux(self, gain: int) -> float: + """Read the lux (light value) from the sensor and return it. Must + specify the gain value to use for the lux reading: + + ================= ===== + Setting Value + ================= ===== + ``ALS_GAIN_1`` 1x + ``ALS_GAIN_1_25`` 1.25x + ``ALS_GAIN_1_67`` 1.67x + ``ALS_GAIN_2_5`` 2.5x + ``ALS_GAIN_5`` 5x + ``ALS_GAIN_10`` 10x + ``ALS_GAIN_20`` 20x + ``ALS_GAIN_40`` 40x + ================= ===== + + :param int gain: The gain value to use + + """ + reg = self._read_8(_VL6180X_REG_SYSTEM_INTERRUPT_CONFIG) + reg &= ~0x38 + reg |= 0x4 << 3 # IRQ on ALS ready + self._write_8(_VL6180X_REG_SYSTEM_INTERRUPT_CONFIG, reg) + # 100 ms integration period + self._write_8(_VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI, 0) + self._write_8(_VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO, 100) + # analog gain + gain = min(gain, ALS_GAIN_40) + self._write_8(_VL6180X_REG_SYSALS_ANALOGUE_GAIN, 0x40 | gain) + # start ALS + self._write_8(_VL6180X_REG_SYSALS_START, 0x1) + # Poll until "New Sample Ready threshold event" is set + while ( + (self._read_8(_VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) >> 3) & 0x7 + ) != 4: + pass + # read lux! + lux = self._read_16(_VL6180X_REG_RESULT_ALS_VAL) + # clear interrupt + self._write_8(_VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07) + lux *= 0.32 # calibrated count/lux + if gain == ALS_GAIN_1: + pass + elif gain == ALS_GAIN_1_25: + lux /= 1.25 + elif gain == ALS_GAIN_1_67: + lux /= 1.67 + elif gain == ALS_GAIN_2_5: + lux /= 2.5 + elif gain == ALS_GAIN_5: + lux /= 5 + elif gain == ALS_GAIN_10: + lux /= 10 + elif gain == ALS_GAIN_20: + lux /= 20 + elif gain == ALS_GAIN_40: + lux /= 40 + lux *= 100 + lux /= 100 # integration time in ms + return lux + + @property + def range_status(self) -> int: + """Retrieve the status/error from a previous range read. This will + return a constant value such as: + + ===================== ============================== + Error Description + ===================== ============================== + ``ERROR_NONE`` No error + ``ERROR_SYSERR_1`` System error 1 (see datasheet) + ``ERROR_SYSERR_5`` System error 5 (see datasheet) + ``ERROR_ECEFAIL`` ECE failure + ``ERROR_NOCONVERGE`` No convergence + ``ERROR_RANGEIGNORE`` Outside range ignored + ``ERROR_SNR`` Too much noise + ``ERROR_RAWUFLOW`` Raw value underflow + ``ERROR_RAWOFLOW`` Raw value overflow + ``ERROR_RANGEUFLOW`` Range underflow + ``ERROR_RANGEOFLOW`` Range overflow + ===================== ============================== + + """ + return self._read_8(_VL6180X_REG_RESULT_RANGE_STATUS) >> 4 + + def _load_settings(self) -> None: + # private settings from page 24 of app note + self._write_8(0x0207, 0x01) + self._write_8(0x0208, 0x01) + self._write_8(0x0096, 0x00) + self._write_8(0x0097, 0xFD) + self._write_8(0x00E3, 0x00) + self._write_8(0x00E4, 0x04) + self._write_8(0x00E5, 0x02) + self._write_8(0x00E6, 0x01) + self._write_8(0x00E7, 0x03) + self._write_8(0x00F5, 0x02) + self._write_8(0x00D9, 0x05) + self._write_8(0x00DB, 0xCE) + self._write_8(0x00DC, 0x03) + self._write_8(0x00DD, 0xF8) + self._write_8(0x009F, 0x00) + self._write_8(0x00A3, 0x3C) + self._write_8(0x00B7, 0x00) + self._write_8(0x00BB, 0x3C) + self._write_8(0x00B2, 0x09) + self._write_8(0x00CA, 0x09) + self._write_8(0x0198, 0x01) + self._write_8(0x01B0, 0x17) + self._write_8(0x01AD, 0x00) + self._write_8(0x00FF, 0x05) + self._write_8(0x0100, 0x05) + self._write_8(0x0199, 0x05) + self._write_8(0x01A6, 0x1B) + self._write_8(0x01AC, 0x3E) + self._write_8(0x01A7, 0x1F) + self._write_8(0x0030, 0x00) + # Recommended : Public registers - See data sheet for more detail + self._write_8(0x0011, 0x10) # Enables polling for 'New Sample ready' + # when measurement completes + self._write_8(0x010A, 0x30) # Set the averaging sample period + # (compromise between lower noise and + # increased execution time) + self._write_8(0x003F, 0x46) # Sets the light and dark gain (upper + # nibble). Dark gain should not be + # changed. + self._write_8(0x0031, 0xFF) # sets the # of range measurements after + # which auto calibration of system is + # performed + self._write_8(0x0040, 0x63) # Set ALS integration time to 100ms + self._write_8(0x002E, 0x01) # perform a single temperature calibration + # of the ranging sensor + + # Optional: Public registers - See data sheet for more detail + self._write_8(0x001B, 0x09) # Set default ranging inter-measurement + # period to 100ms + self._write_8(0x003E, 0x31) # Set default ALS inter-measurement period + # to 500ms + self._write_8(0x0014, 0x24) # Configures interrupt on 'New Sample + # Ready threshold event' + + def _write_8(self, address: int, data: int) -> None: + # Write 1 byte of data from the specified 16-bit register address. + with self._device: + self._device.write(bytes([(address >> 8) & 0xFF, address & 0xFF, data])) + + def _write_16(self, address: int, data: int) -> None: + # Write a 16-bit big endian value to the specified 16-bit register + # address. + with self._device as i2c: + i2c.write( + bytes( + [ + (address >> 8) & 0xFF, + address & 0xFF, + (data >> 8) & 0xFF, + data & 0xFF, + ] + ) + ) + + def _read_8(self, address: int) -> int: + # Read and return a byte from the specified 16-bit register address. + with self._device as i2c: + result = bytearray(1) + i2c.write(bytes([(address >> 8) & 0xFF, address & 0xFF])) + i2c.readinto(result) + return result[0] + + def _read_16(self, address: int) -> int: + # Read and return a 16-bit unsigned big endian value read from the + # specified 16-bit register address. + with self._device as i2c: + result = bytearray(2) + i2c.write(bytes([(address >> 8) & 0xFF, address & 0xFF])) + i2c.readinto(result) + return (result[0] << 8) | result[1] diff --git a/Batt_Board/lib/battery_functions.py b/Batt_Board/lib/battery_functions.py index d34b82cf..a20dc869 100644 --- a/Batt_Board/lib/battery_functions.py +++ b/Batt_Board/lib/battery_functions.py @@ -265,24 +265,6 @@ def face_toggle(self, face, state): elif face == "Face5": self.cubesat.Face0.duty_cycle = duty_cycle - def all_face_data(self): - - self.cubesat.all_faces_on() - try: - import Big_Data - - a = Big_Data.AllFaces(self.debug, self.cubesat.tca) - - self.facestring = a.Face_Test_All() - - del a - del Big_Data - - except Exception as e: - self.debug_print("Big_Data error" + "".join(traceback.format_exception(e))) - - return self.facestring - def get_imu_data(self): self.cubesat.all_faces_on() diff --git a/Batt_Board/lib/neopixel.mpy b/Batt_Board/lib/neopixel.mpy new file mode 100755 index 0000000000000000000000000000000000000000..e79666f32d940cbbc51a02556fc36be931333279 GIT binary patch literal 1318 zcmY*WT~ixX7=AaOOB&(iWH|&9X-XiJ010eES|PTOY|4iS1VPMbXGt~-*)e2yHk(53 zHmM)Fxwf2Ce<{qF8S>5V@1k}A!bfKf5X}}e9<*;N_*7P>g&4orxZO#Sc6G`q$A$F*S;s5-H`EpGLO!r&}0*yU4mF|Gj(u~>fl?+3x z&c|X@^ir)5rTb%7GRKy#^iC_fM0MY)8zr?OUda-rJ-}LLqciq_D;~wI&ArUFxVrIf zW_u?ifW7zs8F;)t6=zJR=?sU%>-SUq)TT6A431uW`)1Z}Ar^XCiItujav!MO3cR}1Sjxm)SN%uubRUju|@iyI*a?@xlQgM zOx}2B3LYV@5Moe&VH=S`77Y|yH-I5No+0DBg+zEOndEI`$}`W~;|!VM9b}ew622v| zpOwG*!aQAE^erSqGh9CzF!fM$BD^*loZg6xh9aSn$Zon8jGU!6zJK&m5O7~Ag*`7) zIsE1sz8kj)TRfb_nP1T$H}q42d68PowtaqV@dmuFo4cR>Dg0sLd)SJGvvKRwm%=Yy zFJz(KZ|B*+em_sIbf#3xLy)AqE}7Q!~~sCTUG4f(>9y~fNEduze0O+rh2tLHgJ?cErcnvEp%DH7CNyon literal 0 HcmV?d00001 diff --git a/Batt_Board/lib/pysquared_eps.py b/Batt_Board/lib/pysquared_eps.py index f2e252db..d1626fd6 100644 --- a/Batt_Board/lib/pysquared_eps.py +++ b/Batt_Board/lib/pysquared_eps.py @@ -1,6 +1,6 @@ """ CircuitPython driver for PySquared satellite board. -PySquared Hardware Version: mainboard-v01 +PySquared Hardware Version: batteryboard v3c CircuitPython Version: 9.0.0 alpha Library Repo: @@ -17,8 +17,9 @@ # Hardware Specific Libs import neopixel # RGB LED import adafruit_pca9685 # LED Driver -import adafruit_tca9548a # I2C Multiplexer import adafruit_pct2075 # Temperature Sensor +import adafruit_max31856 # Thermocouple +import adafruit_vl6180x # LiDAR Distance Sensor for Antenna import adafruit_ina219 # Power Monitor # CAN Bus Import @@ -29,10 +30,6 @@ from bitflags import bitFlag, multiBitFlag, multiByte from micropython import const -# Thermoucouple ADC -import adafruit_ads1x15.ads1015 as ADS -from adafruit_ads1x15.analog_in import AnalogIn - # NVM register numbers _BOOTCNT = const(0) _VBUSRST = const(6) @@ -77,6 +74,7 @@ def all_faces_on(self): self.hardware["Face3"] = True self.Face4.duty_cycle = 0xFFFF self.hardware["Face4"] = True + self.cam.duty_cycle = 0xFFFF def all_faces_off(self): # De-Power Faces @@ -96,6 +94,8 @@ def all_faces_off(self): self.Face4.duty_cycle = 0x0000 time.sleep(0.1) self.hardware["Face4"] = False + time.sleep(0.1) + self.cam.duty_cycle = 0x0000 def debug_print(self, statement): if self.debug: @@ -123,13 +123,13 @@ def __init__(self): self.hardware = { "WDT": False, "NEO": False, - "TCA": False, "SOLAR": False, "PWR": False, "FLD": False, "TEMP": False, "COUPLE": False, "CAN": False, + "LIDAR": False, "Face0": False, "Face1": False, "Face2": False, @@ -143,18 +143,53 @@ def __init__(self): self._resetReg = digitalio.DigitalInOut(board.VBUS_RESET) self._resetReg.switch_to_output(drive_mode=digitalio.DriveMode.OPEN_DRAIN) + # Define 5V Enable + self._5V_enable = digitalio.DigitalInOut(board.ENABLE_5V) + self._5V_enable.switch_to_output(drive_mode=digitalio.DriveMode.OPEN_DRAIN) + try: + self._5V_enable.value = True + self.debug_print("5V Enabled") + except Exception as e: + self.debug_print( + "Error Setting 5V Enable: " + "".join(traceback.format_exception(e)) + ) + # Define SPI,I2C,UART | paasing I2C1 to BigData try: self.i2c0 = busio.I2C(board.I2C0_SCL, board.I2C0_SDA, timeout=5) + except Exception as e: + self.debug_print( + "ERROR INITIALIZING I2C0: " + "".join(traceback.format_exception(e)) + ) + + try: self.spi0 = busio.SPI(board.SPI0_SCK, board.SPI0_MOSI, board.SPI0_MISO) + except Exception as e: + self.debug_print( + "ERROR INITIALIZING SPI0: " + "".join(traceback.format_exception(e)) + ) + + try: self.i2c1 = busio.I2C( board.I2C1_SCL, board.I2C1_SDA, timeout=5, frequency=100000 ) + except Exception as e: + self.debug_print( + "ERROR INITIALIZING I2C1: " + "".join(traceback.format_exception(e)) + ) + + try: self.spi1 = busio.SPI(board.SPI1_SCK, board.SPI1_MOSI, board.SPI1_MISO) + except Exception as e: + self.debug_print( + "ERROR INITIALIZING SPI1: " + "".join(traceback.format_exception(e)) + ) + + try: self.uart = busio.UART(board.TX, board.RX, baudrate=self.urate) except Exception as e: self.debug_print( - "ERROR INITIALIZING BUSSES: " + "".join(traceback.format_exception(e)) + "ERROR INITIALIZING UART: " + "".join(traceback.format_exception(e)) ) # Initialize LED Driver @@ -174,16 +209,13 @@ def __init__(self): self.Face2 = self.faces.channels[2] self.Face3 = self.faces.channels[3] self.Face4 = self.faces.channels[4] + self.cam = self.faces.channels[5] self.all_faces_on() except Exception as e: self.debug_print( "ERROR INITIALIZING FACES: " + "".join(traceback.format_exception(e)) ) - # Define I2C Reset - self._i2c_reset = digitalio.DigitalInOut(board.I2C_RESET) - self._i2c_reset.switch_to_output(value=True) - if self.c_boot > 200: self.c_boot = 0 @@ -191,28 +223,18 @@ def __init__(self): self.f_softboot = False # Define radio - _rf_cs1 = digitalio.DigitalInOut(board.SPI0_CS0) - self.enable_rf = digitalio.DigitalInOut(board.RF_ENABLE) + self.enable_rf = digitalio.DigitalInOut(board.ENABLE_RF) # self.enable_rf.switch_to_output(value=False) # if U21 self.enable_rf.switch_to_output(value=True) # if U7 - _rf_cs1.switch_to_output(value=True) # Define Heater Pins - try: - if self.hardware["FLD"]: - self.heater = self.faces.channels[15] - except Exception as e: - self.debug_print( - "[WARNING][Battery_Heater]" + "".join(traceback.format_exception(e)) - ) + self.heater = pwmio.PWMOut(board.ENABLE_HEATER, frequency=1000, duty_cycle=0) # Initialize Neopixel try: - self.neopwr = digitalio.DigitalInOut(board.NEO_PWR) - self.neopwr.switch_to_output(value=True) self.neopixel = neopixel.NeoPixel( - board.NEOPIXEL, 1, brightness=0.2, pixel_order=neopixel.GRB + board.NEOPIX, 1, brightness=0.2, pixel_order=neopixel.GRB ) self.neopixel[0] = (0, 0, 255) self.hardware["NEO"] = True @@ -241,8 +263,15 @@ def __init__(self): "[ERROR][SOLAR Power Monitor]" + "".join(traceback.format_exception(e)) ) + # Initialize LiDAR + try: + self.LiDAR = adafruit_vl6180x.VL6180X(self.i2c1, offset=0) + self.hardware["LiDAR"] = True + except Exception as e: + self.debug_print("[ERROR][LiDAR]" + "".join(traceback.format_exception(e))) + # Define Charge Indicate Pin - self.charge_indicate = digitalio.DigitalInOut(board.IS_CHARGING) + self.charge_indicate = digitalio.DigitalInOut(board.CHRG) self.charge_indicate.switch_to_input(pull=digitalio.Pull.DOWN) # Initialize PCT2075 Temperature Sensor @@ -255,8 +284,11 @@ def __init__(self): ) # Initialize Thermocouple ADC + self.spi1_cs0 = digitalio.DigitalInOut(board.SPI1_CS0) + self.spi1_cs0.direction = digitalio.Direction.OUTPUT + try: - self.thermocouple = ADS.ADS1015(self.i2c0, address=0x48) + self.thermocouple = adafruit_max31856.MAX31856(self.spi1, self.spi1_cs0) self.hardware["COUPLE"] = True self.debug_print("[ACTIVE][Thermocouple]") except Exception as e: @@ -264,24 +296,11 @@ def __init__(self): "[ERROR][THERMOCOUPLE]" + "".join(traceback.format_exception(e)) ) - # Initialize TCA - try: - self.tca = adafruit_tca9548a.TCA9548A(self.i2c0, address=int(0x77)) - for channel in range(8): - if self.tca[channel].try_lock(): - self.debug_print("Channel {}:".format(channel)) - addresses = self.tca[channel].scan() - print([hex(address) for address in addresses if address != 0x70]) - self.tca[channel].unlock() - self.hardware["TCA"] = True - except Exception as e: - self.debug_print("[ERROR][TCA]" + "".join(traceback.format_exception(e))) - # Initialize CAN Transceiver try: - self.spi1cs0 = digitalio.DigitalInOut(board.SPI1_CS0) - self.spi1cs0.switch_to_output() - self.can_bus = CAN(self.spi1, self.spi1cs0, loopback=True, silent=True) + self.spi0cs0 = digitalio.DigitalInOut(board.SPI0_CS0) + self.spi0cs0.switch_to_output() + self.can_bus = CAN(self.spi0, self.spi0cs0, loopback=True, silent=True) self.hardware["CAN"] = True except Exception as e: @@ -289,8 +308,24 @@ def __init__(self): "[ERROR][CAN TRANSCEIVER]" + "".join(traceback.format_exception(e)) ) - # Prints init state of PySquared hardware - self.debug_print(str(self.hardware)) + """ + Prints init State of PySquared Hardware + """ + self.debug_print("PySquared Hardware Initialization Complete!") + + if self.debug: + # Find the length of the longest key + max_key_length = max(len(key) for key in self.hardware.keys()) + + print("=" * 16) + print("Device | Status") + for key, value in self.hardware.items(): + padded_key = key + " " * (max_key_length - len(key)) + if value: + print(co(f"|{padded_key} | {value} |", "green")) + else: + print(co(f"|{padded_key} | {value}|", "red")) + print("=" * 16) # set PyCubed power mode self.power_mode = "normal" @@ -321,12 +356,12 @@ def RGB(self, value): self.debug_print("[WARNING] neopixel not initialized") # =======================================================# - # Before Flight Flags # + # Before Flight Flags # # =======================================================# - # These flags should be set as follows before flight: # - # burnarm = True # - # burned = False # - # dist = 0 # + # These flags should be set as follows before flight: # + # burnarm = True # + # burned = False # + # dist = 0 # # =======================================================# @property def burnarm(self): @@ -355,6 +390,7 @@ def dist(self, value): def arm_satellite(self): self.burnarm = True self.burned = False + self.f_triedburn = False self.dist = 0 print("[Satellite Armed]") @@ -576,6 +612,20 @@ def uptime(self): self.CURRENTTIME = const(time.time()) return self.CURRENTTIME - self.BOOTTIME + @property + def fc_wdt(self): + return self._5V_enable.value + + @fc_wdt.setter + def fc_wdt(self, value): + try: + self._5V_enable.value = value + except Exception as e: + self.debug_print( + "Error Setting FC Watchdog Status: " + + "".join(traceback.format_exception(e)) + ) + @property def reset_vbus(self): try: @@ -587,7 +637,7 @@ def reset_vbus(self): ) # =======================================================# - # Thermal Management # + # Thermal Management # # =======================================================# @property def internal_temperature(self): @@ -596,34 +646,29 @@ def internal_temperature(self): @property def battery_temperature(self): if self.hardware["COUPLE"]: - chan = AnalogIn(self.thermocouple, ADS.P1) - tip = (chan.voltage - 1.25) / 0.005 - return tip + return self.thermocouple.temperature else: self.debug_print("[WARNING] Thermocouple not initialized") def heater_on(self): - if self.hardware["FLD"]: - try: - self._relayA.drive_mode = digitalio.DriveMode.PUSH_PULL - if self.f_brownout: - pass - else: - self.f_brownout = True - self.heating = True - self._relayA.value = 1 - self.RGB = (255, 165, 0) - # Pause to ensure relay is open - time.sleep(0.25) - self.heater.duty_cycle = 0x7FFF - except Exception as e: - self.debug_print( - "[ERROR] Cant turn on heater: " - + "".join(traceback.format_exception(e)) - ) - self.heater.duty_cycle = 0x0000 - else: - self.debug_print("[WARNING] LED Driver not initialized") + + try: + self._relayA.drive_mode = digitalio.DriveMode.PUSH_PULL + if self.f_brownout: + pass + else: + self.f_brownout = True + self.heating = True + self._relayA.value = 1 + self.RGB = (255, 165, 0) + # Pause to ensure relay is open + time.sleep(0.25) + self.heater.duty_cycle = 0x7FFF + except Exception as e: + self.debug_print( + "[ERROR] Cant turn on heater: " + "".join(traceback.format_exception(e)) + ) + self.heater.duty_cycle = 0x0000 def heater_off(self): if self.hardware["FLD"]: @@ -645,6 +690,28 @@ def heater_off(self): else: self.debug_print("[WARNING] LED Driver not initialized") + # =======================================================# + # Burn Wire # + # =======================================================# + + def distance(self): + if self.hardware["LiDAR"]: + try: + distance_mm = 0 + for _ in range(10): + distance_mm += self.LiDAR.range + time.sleep(0.01) + self.debug_print("distance measured = {0}mm".format(distance_mm / 10)) + return distance_mm / 10 + except Exception as e: + self.debug_print( + "LiDAR error: " + "".join(traceback.format_exception(e)) + ) + return 0 + else: + self.debug_print("[WARNING] LiDAR not initialized") + return 0 + def burn(self, burn_num, dutycycle=0, freq=1000, duration=1): """ Operate burn wire circuits. Wont do anything unless the a nichrome burn wire @@ -702,6 +769,135 @@ def burn(self, burn_num, dutycycle=0, freq=1000, duration=1): burnwire.deinit() self._relayA.drive_mode = digitalio.DriveMode.OPEN_DRAIN + def smart_burn(self, burn_num, dutycycle=0.1): + """ + Operate burn wire circuits. Wont do anything unless the a nichrome burn wire + has been installed. + + IMPORTANT: See "Burn Wire Info & Usage" of https://pycubed.org/resources + before attempting to use this function! + + burn_num: (string) which burn wire circuit to operate, must be either '1' or '2' + dutycycle: (float) duty cycle percent, must be 0.0 to 100 + freq: (float) frequency in Hz of the PWM pulse, default is 1000 Hz + duration: (float) duration in seconds the burn wire should be on + """ + + freq = 1000 + + distance1 = 0 + distance2 = 0 + # self.dist=self.distance() + + try: + # convert duty cycle % into 16-bit fractional up time + dtycycl = int((dutycycle / 100) * (0xFFFF)) + self.debug_print("----- SMART BURN WIRE CONFIGURATION -----") + self.debug_print( + "\tFrequency of: {}Hz\n\tDuty cycle of: {}% (int:{})".format( + freq, (100 * dtycycl / 0xFFFF), dtycycl + ) + ) + # create our PWM object for the respective pin + # not active since duty_cycle is set to 0 (for now) + if "1" in burn_num: + burnwire = pwmio.PWMOut(board.ENABLE_BURN, frequency=freq, duty_cycle=0) + else: + return False + + try: + distance1 = self.distance() + self.debug_print(str(distance1)) + if ( + distance1 > self.dist + 2 + and distance1 > 4 + or self.f_triedburn == True + ): + self.burned = True + self.f_brownout = True + raise TypeError( + "Wire seems to have burned and satellite browned out" + ) + else: + self.dist = int(distance1) + self.burnarm = True + if self.burnarm: + self.burnarm = False + self.f_triedburn = True + + # Configure the relay control pin & open relay + self.RGB = (0, 165, 0) + + self._relayA.drive_mode = digitalio.DriveMode.PUSH_PULL + self.RGB = (255, 165, 0) + self._relayA.value = 1 + + # Pause to ensure relay is open + time.sleep(0.5) + + # Start the Burn + burnwire.duty_cycle = dtycycl + + # Burn Timer + start_time = time.monotonic() + + # Monitor the burn + while not self.burned: + distance2 = self.distance() + self.debug_print(str(distance2)) + if distance2 > distance1 + 1 or distance2 > 10: + self._relayA.value = 0 + burnwire.duty_cycle = 0 + self.burned = True + self.f_triedburn = False + else: + distance1 = distance2 + time_elapsed = time.monotonic() - start_time + print("Time Elapsed: " + str(time_elapsed)) + if time_elapsed > 4: + self._relayA.value = 0 + burnwire.duty_cycle = 0 + self.burned = False + self.RGB = (0, 0, 255) + time.sleep(10) + self.f_triedburn = False + break + + time.sleep(5) + distance2 = self.distance() + else: + pass + if distance2 > distance1 + 2 or distance2 > 10: + self.burned = True + self.f_triedburn = False + except Exception as e: + self.debug_print( + "Error in Burn Sequence: " + "".join(traceback.format_exception(e)) + ) + self.debug_print("Error: " + str(e)) + if "no attribute 'LiDAR'" in str(e): + self.debug_print("Burning without LiDAR") + time.sleep(120) # Set to 120 for flight + self.burnarm = False + self.burned = True + self.f_triedburn = True + self.burn("1", dutycycle, freq, 4) + time.sleep(5) + + finally: + self._relayA.value = 0 + burnwire.duty_cycle = 0 + self.RGB = (0, 0, 0) + burnwire.deinit() + self._relayA.drive_mode = digitalio.DriveMode.OPEN_DRAIN + + return True + except Exception as e: + self.debug_print( + "Error with Burn Wire: " + "".join(traceback.format_exception(e)) + ) + return False + print("Initializing Power Management Systems...") cubesat = Satellite() diff --git a/FC_Board/cdh.py b/FC_Board/cdh.py index 1ddf21b4..245dde8a 100644 --- a/FC_Board/cdh.py +++ b/FC_Board/cdh.py @@ -12,7 +12,8 @@ ] # our 4 byte code to authorize commands # pass-code for DEMO PURPOSES ONLY -super_secret_code = b"" # put your own code here +super_secret_code = b"ABCD" # put your own code here +repeat_code = b"RP" print(f"Super secret code is: {super_secret_code}") commands = { b"\x8eb": "noop", @@ -98,6 +99,12 @@ def message_handler(cubesat, msg): if response is not None: cubesat.c_gs_resp += 1 message_handler(cubesat, response) + elif bytes(msg[4:6]) == repeat_code: + print("Repeating last message!") + try: + cubesat.radio1.send(msg[6:]) + except Exception as e: + print("error repeating message: {}".format(e)) else: print("bad code?") @@ -112,7 +119,7 @@ def hreset(cubesat): print("Resetting") try: cubesat.radio1.send(data=b"resetting") - cubesat.micro.on_next_reset(self.cubesat.micro.RunMode.NORMAL) + cubesat.micro.on_next_reset(cubesat.micro.RunMode.NORMAL) cubesat.micro.reset() except: pass diff --git a/FC_Board/code.py b/FC_Board/code.py index 4eba7a01..ede841bd 100644 --- a/FC_Board/code.py +++ b/FC_Board/code.py @@ -4,21 +4,24 @@ """ Built for the PySquared FC Board -Version: 1.0.1 (Beta) -Published: July 26, 2024 +Version: 2.0.0 +Published: Nov 19, 2024 """ import time +import microcontroller print("=" * 70) print("Hello World!") -print("PySquared FC Board Circuit Python Software Version: 1.0.1 (Beta)") -print("Published: July 26, 2024") +print("PySquared FC Board Circuit Python Software Version: 2.0.0") +print("Published: November 19, 2024") print("=" * 70) +loiter_time = 5 + try: - for i in range(10): - print(f"Code Starting in {10-i} seconds") + for i in range(loiter_time): + print(f"Code Starting in {loiter_time-i} seconds") time.sleep(1) import main diff --git a/FC_Board/lib/Big_Data.py b/FC_Board/lib/Big_Data.py new file mode 100755 index 00000000..f204b45c --- /dev/null +++ b/FC_Board/lib/Big_Data.py @@ -0,0 +1,95 @@ +from debugcolor import co +import time +import traceback +import gc + + +class Face: + def __init__(self, Add, Pos, debug_state, tca): + self.tca = tca + self.address = Add + self.position = Pos + self.debug = debug_state + + # Use tuple instead of list for immutable data + self.senlist = () + # Define sensors based on position using a dictionary lookup instead of if-elif chain + sensor_map = { + "x+": ("MCP", "VEML", "DRV"), + "x-": ("MCP", "VEML"), + "y+": ("MCP", "VEML", "DRV"), + "y-": ("MCP", "VEML"), + "z-": ("MCP", "VEML", "DRV"), + } + self.senlist = sensor_map.get(Pos, ()) + + # Initialize sensor states dict only with needed sensors + self.sensors = {sensor: False for sensor in self.senlist} + + # Initialize sensor objects as None + self.mcp = None + self.veml = None + self.drv = None + + def debug_print(self, statement): + if self.debug: + print(co("[FACE]" + statement, "teal", "bold")) + + def Sensorinit(self, senlist, address): + gc.collect() # Force garbage collection before initializing sensors + + if "MCP" in senlist: + try: + import adafruit_mcp9808 + + self.mcp = adafruit_mcp9808.MCP9808(self.tca[address], address=27) + self.sensors["MCP"] = True + except Exception as e: + self.debug_print("[ERROR][Temperature Sensor]" + str(e)) + + if "VEML" in senlist: + try: + import adafruit_veml7700 + + self.veml = adafruit_veml7700.VEML7700(self.tca[address]) + self.sensors["VEML"] = True + except Exception as e: + self.debug_print("[ERROR][Light Sensor]" + str(e)) + + if "DRV" in senlist: + try: + import adafruit_drv2605 + + self.drv = adafruit_drv2605.DRV2605(self.tca[address]) + self.sensors["DRV"] = True + except Exception as e: + self.debug_print("[ERROR][Motor Driver]" + str(e)) + + gc.collect() # Clean up after initialization + + +class AllFaces: + def __init__(self, debug, tca): + self.tca = tca + self.debug = debug + self.faces = [] + + # Create faces using a loop instead of individual variables + positions = [("y+", 0), ("y-", 1), ("x+", 2), ("x-", 3), ("z-", 4)] + for pos, addr in positions: + face = Face(addr, pos, debug, tca) + face.Sensorinit(face.senlist, face.address) + self.faces.append(face) + gc.collect() # Clean up after each face initialization + + def Face_Test_All(self): + results = [] + for face in self.faces: + if face: + try: + temp = face.mcp.temperature if face.sensors.get("MCP") else None + light = face.veml.lux if face.sensors.get("VEML") else None + results.append([temp, light]) + except Exception as e: + results.append([None, None]) + return results diff --git a/FC_Board/lib/Field.py b/FC_Board/lib/Field.py index feb483bb..5ab3c661 100755 --- a/FC_Board/lib/Field.py +++ b/FC_Board/lib/Field.py @@ -18,29 +18,15 @@ def debug_print(self, statement): def __init__(self, cubesat, debug): self.debug = debug self.cubesat = cubesat - try: - if self.cubesat.legacy: - 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 - if self.cubesat.radio1.spreading_factor > 8: - self.cubesat.radio1.low_datarate_optimize = True - except Exception as e: - self.debug_print( - "Error Defining Radio features: " - + "".join(traceback.format_exception(e)) - ) def Beacon(self, msg): try: if self.cubesat.is_licensed: self.debug_print("I am beaconing: " + str(msg)) - self.cubesat.radio1.send(msg) + print( + "Message Success: " + + str(self.cubesat.radio1.send(bytes(msg, "UTF-8"))) + ) else: self.debug_print( "Please toggle licensed variable in code once you obtain an amateur radio license" diff --git a/FC_Board/lib/adafruit_mcp2515/__init__.py b/FC_Board/lib/adafruit_mcp2515/__init__.py new file mode 100644 index 00000000..45490f6c --- /dev/null +++ b/FC_Board/lib/adafruit_mcp2515/__init__.py @@ -0,0 +1,943 @@ +# SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_mcp2515` +================================================================================ + +A CircuitPython library for working with the MCP2515 CAN bus controller using the +CircuitPython `canio` API + + +* Author(s): Bryan Siepert + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +""" + +from collections import namedtuple +from struct import unpack_from, pack_into +from time import sleep +from micropython import const +from adafruit_bus_device import spi_device +from .canio import * +from .timer import Timer + +try: + from typing_extensions import Literal +except ImportError: + pass + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MCP2515.git" + +# modes +_MODE_NORMAL = const(0x00) +_MODE_SLEEP = const(0x20) +_MODE_LOOPBACK = const(0x40) +_MODE_LISTENONLY = const(0x60) +_MODE_CONFIG = const(0x80) + +# commands +_RESET = const(0xC0) +_WRITE = const(0x02) +_READ = const(0x03) +_BITMOD = const(0x05) + +_LOAD_TX0 = const(0x40) +_LOAD_TX1 = const(0x42) +_LOAD_TX2 = const(0x44) +_READ_STATUS = const(0xA0) + +_SEND_TX0 = const(0x81) +_SEND_TX1 = const(0x82) +_SEND_TX2 = const(0x84) +_SEND_ALL = const(0x87) + +_READ_RX0 = const(0x90) +_READ_RX1 = const(0x94) + +# Registers + +_CANINTE = const(0x2B) +_CANINTF = const(0x2C) +_CANSTAT = const(0x0E) +_CANCTRL = const(0x0F) + +_CNF3 = const(0x28) +_CNF2 = const(0x29) +_CNF1 = const(0x2A) + +_TXB0CTRL = const(0x30) +_TXB0SIDH = const(0x31) + +_TXB1CTRL = const(0x40) +_TXB1SIDH = const(0x41) + +_TXB2CTRL = const(0x50) +_TXB2SIDH = const(0x51) + +_RXB0CTRL = const(0x60) +_RXB0SIDH = const(0x61) + +_RXB1CTRL = const(0x70) +_RXB1SIDH = const(0x71) + +_TX0IF = const(0x04) +_TX1IF = const(0x08) +_TX2IF = const(0x10) + +# Filters & Masks +_RXM0SIDH = const(0x20) +_RXM1SIDH = const(0x24) +MASKS = [_RXM0SIDH, _RXM1SIDH] + +_RXF0SIDH = const(0x00) +_RXF1SIDH = const(0x04) +_RXF2SIDH = const(0x08) +_RXF3SIDH = const(0x10) +_RXF4SIDH = const(0x14) +_RXF5SIDH = const(0x18) +FILTERS = [[_RXF0SIDH, _RXF1SIDH], [_RXF2SIDH, _RXF3SIDH, _RXF4SIDH, _RXF5SIDH]] +# bits/flags +_RX0IF = const(0x01) +_RX1IF = const(0x02) +_WAKIF = const(0x40) +# _MERRF = const(0x80) + +# Standard/Extended ID Buffers, Masks, Flags +_TXB_EXIDE_M_16 = const(0x08) +_TXB_TXREQ_M = const(0x08) # TX request/completion bit + +EXTID_TOP_11_WRITE_MASK = 0x1FFC0000 +EXTID_TOP_11_READ_MASK = 0xFFE00000 + +EXTID_BOTTOM_29_MASK = (1 << 29) - 1 # bottom 18 bits +EXTID_BOTTOM_18_MASK = (1 << 18) - 1 # bottom 18 bits +STDID_BOTTOM_11_MASK = 0x7FF + +EXTID_FLAG_MASK = ( + 1 << 19 +) # to set/get the "is an extended id?" flag from a 4-byte ID buffer + +# masks +_MODE_MASK = const(0xE0) + +_RXB_RX_MASK = const(0x60) +_RXB_BUKT_MASK = const((1 << 2)) +_RXB_RX_STDEXT = const(0x00) + +_STAT_RXIF_MASK = const(0x03) +_RTR_MASK = const(0x40) + +_STAT_TXIF_MASK = const(0xA8) +_STAT_TX0_PENDING = const(0x04) +_STAT_TX1_PENDING = const(0x10) +_STAT_TX2_PENDING = const(0x40) + +_STAT_TX_PENDING_MASK = const(_STAT_TX0_PENDING | _STAT_TX1_PENDING | _STAT_TX2_PENDING) + +###### Bus State and Error Counts ########## + +# TEC: TRANSMIT ERROR COUNTER REGISTER (ADDRESS: 1Ch) +_TEC = const(0x1C) +_REC = const(0x1D) +# REC: RECEIVE ERROR COUNTER REGISTER (ADDRESS: 1Dh) +_EFLG = const(0x2D) + +############ Misc Consts ######### +_SEND_TIMEOUT_MS = const(5) # 500ms +_MAX_CAN_MSG_LEN = 8 # ?! +# perhaps this will be stateful later? +_TransmitBuffer = namedtuple( + "_TransmitBuffer", + ["CTRL_REG", "STD_ID_REG", "INT_FLAG_MASK", "LOAD_CMD", "SEND_CMD"], +) + +# --- Baud Rates Table --- +# Values for the 8MHz and 10MHz Crystal Oscillator are based on this calculator: +# https://www.kvaser.com/support/calculators/bit-timing-calculator/ +# - MCP2510 can be used to calculate the timing values since the registers are allmost the same +# Only difference is the CNF3, which has an extra bit (CNF3[7] = SOF, Start of Frame signal bit) +# The SOF bit can be left on zero +# - A bit sample point (SP%) of 70% is used if nothing else is defined +# - A Synchronization Jump Width (SJW) of 1 Time Quanta (TQ) is used + +_BAUD_RATES = { + # This is magic, don't disturb the dragon + # expects a 16Mhz crystal + 16000000: { + # CNF1, CNF2, CNF3 + 1000000: (0x00, 0xD0, 0x82), + 500000: (0x00, 0xF0, 0x86), + 250000: (0x41, 0xF1, 0x85), + 200000: (0x01, 0xFA, 0x87), + 125000: (0x03, 0xF0, 0x86), + 100000: (0x03, 0xFA, 0x87), + 95000: (0x03, 0xAD, 0x07), + 83300: (0x03, 0xBE, 0x07), + 80000: (0x03, 0xFF, 0x87), + 50000: (0x07, 0xFA, 0x87), + 40000: (0x07, 0xFF, 0x87), + 33000: (0x09, 0xBE, 0x07), + 31250: (0x0F, 0xF1, 0x85), + 25000: (0x0F, 0xBA, 0x07), + 20000: (0x0F, 0xFF, 0x87), + 10000: (0x1F, 0xFF, 0x87), + 5000: (0x3F, 0xFF, 0x87), + 666000: (0x00, 0xA0, 0x04), + }, + # 10MHz Crystal oscillator (used on the MIKROE "CAN SPI click"-board) + 10000000: { + # CNF1, CNF2, CNF3 + 500000: (0x00, 0x92, 0x02), + 250000: (0x00, 0xB5, 0x05), + 200000: (0x00, 0xBF, 0x07), # SP is 68%! + 125000: (0x01, 0xA4, 0x04), # SP is 68.75%! + 100000: (0x01, 0xB5, 0x05), + 95000: (0x05, 0x89, 0x01), # SP is 71.43%, Baud rate is 95.238kbps! + 83300: (0x02, 0xA4, 0x04), # SP is 68.75%, Baud rate is 83.333kbps! + 80000: (0x04, 0x92, 0x02), + 50000: (0x03, 0xB5, 0x05), + 40000: (0x04, 0xB5, 0x05), + 33000: (0x05, 0xB5, 0x05), # Baud rate is 33.333kbps! + 31250: (0x07, 0xA4, 0x04), # SP is 68.75%! + 25000: (0x07, 0xB5, 0x05), + 20000: (0x09, 0xB5, 0x05), + 10000: (0x13, 0xB5, 0x05), + 5000: (0x27, 0xB5, 0x05), + }, + # 8MHz Crystal oscillator + 8000000: { + # CNF1, CNF2, CNF3 + 500000: (0x00, 0x91, 0x01), + 250000: (0x40, 0xB5, 0x01), + 200000: (0x00, 0xB6, 0x04), + 125000: (0x01, 0xAC, 0x03), + 100000: (0x01, 0xB6, 0x04), + 95000: (0x41, 0xBE, 0x04), + 83300: (0x02, 0xAC, 0x03), + 80000: (0x04, 0x9A, 0x01), + 50000: (0x03, 0xB6, 0x04), + 40000: (0x04, 0xB6, 0x04), + 33000: (0x0A, 0x9A, 0x02), + 31250: (0x07, 0xAC, 0x03), + 25000: (0x07, 0xB6, 0x04), + 20000: (0x09, 0xB6, 0x04), + 10000: (0x13, 0xB6, 0x04), + 5000: (0x27, 0xB6, 0x04), + 666000: (0x00, 0x88, 0x01), + }, +} + + +def _tx_buffer_status_decode(status_byte): + out_str = "Status: " + # when CAN_H is disconnected?: 0x18 + out_str += "\nStatus of chosen buffer: %s\n" % hex(status_byte) + if status_byte & 0x40: + out_str += " Message ABORTED" + if status_byte & 0x20: + out_str += " Message LOST ARBITRATION" + if status_byte & 0x10: + out_str += " TRANSMIT ERROR" + if status_byte & 0x8: + out_str += " Transmit Requested" + else: + out_str += " Message sent" + out_str += " Priority: " + ["LAST", "LOW", "MEDIUM", "HIGH"][status_byte & 0x3] + + return out_str + + +class MCP2515: # pylint:disable=too-many-instance-attributes + """A common shared-bus protocol. + + :param ~busio.SPI spi: The SPI bus used to communicate with the MCP2515 + :param ~digitalio.DigitalInOut cs_pin: SPI bus enable pin + :param int baudrate: The bit rate of the bus in Hz. All devices on\ + the bus must agree on this value. Defaults to 250000. + :param Literal crystal_freq: MCP2515 crystal frequency. Valid values are:\ + 16000000, 10000000 and 8000000. Defaults to 16000000 (16MHz).\ + :param bool loopback: Receive only packets sent from this device, and send only to this\ + device. Requires that `silent` is also set to `True`, but only prevents transmission to\ + other devices. Otherwise the send/receive behavior is normal. + :param bool silent: When `True` the controller does not transmit and all messages are\ + received, ignoring errors and filters. This mode can be used to “sniff” a CAN bus without\ + interfering. Defaults to `False`. + :param bool auto_restart: **Not supported by hardware. An `AttributeError` will be raised\ + if `auto_restart` is set to `True`** If `True`, will restart communications after entering\ + bus-off state. Defaults to `False`. + :param bool debug: If `True`, will enable printing debug information. Defaults to `False`. + """ + + def __init__( + self, + spi_bus, + cs_pin, + *, + baudrate: int = 250000, + crystal_freq: Literal[8000000, 10000000, 16000000] = 16000000, + loopback: bool = False, + silent: bool = False, + auto_restart: bool = False, + debug: bool = False, + ): + + if loopback and not silent: + raise AttributeError("Loopback mode requires silent to be set") + if auto_restart: + raise AttributeError("`auto-restart` is not supported by hardware") + + self._auto_restart = auto_restart + self._debug = debug + self._bus_device_obj = spi_device.SPIDevice(spi_bus, cs_pin) + self._cs_pin = cs_pin + self._buffer = bytearray(20) + self._id_buffer = bytearray(4) + self._unread_message_queue = [] + self._timer = Timer() + self._tx_buffers = [] + self._rx0_overflow = False + self._rx1_overflow = False + self._masks_in_use = [] + self._filters_in_use = [[], []] + self._mode = None + self._bus_state = BusState.ERROR_ACTIVE + self._baudrate = baudrate + self._crystal_freq = crystal_freq + self._loopback = loopback + self._silent = silent + + self._init_buffers() + self.initialize() + + def _init_buffers(self): + + self._tx_buffers = [ + _TransmitBuffer( + CTRL_REG=_TXB0CTRL, + STD_ID_REG=_TXB0SIDH, + INT_FLAG_MASK=_TX0IF, + LOAD_CMD=_LOAD_TX0, + SEND_CMD=_SEND_TX0, + ), + _TransmitBuffer( + CTRL_REG=_TXB1CTRL, + STD_ID_REG=_TXB1SIDH, + INT_FLAG_MASK=_TX1IF, + LOAD_CMD=_LOAD_TX1, + SEND_CMD=_SEND_TX1, + ), + _TransmitBuffer( + CTRL_REG=_TXB2CTRL, + STD_ID_REG=_TXB2SIDH, + INT_FLAG_MASK=_TX2IF, + LOAD_CMD=_LOAD_TX2, + SEND_CMD=_SEND_TX2, + ), + ] + + def initialize(self): + """Return the sensor to the default configuration""" + self._reset() + # our mode set skips checking for sleep + self._set_mode(_MODE_CONFIG) + + self._set_baud_rate() + + # intialize TX and RX registers + for idx in range(14): + self._set_register(_TXB0CTRL + idx, 0) + self._set_register(_TXB1CTRL + idx, 0) + self._set_register(_TXB2CTRL + idx, 0) + + self._set_register(_RXB0CTRL, 0) + self._set_register(_RXB1CTRL, 0) + + # # # interrupt mode + # TODO: WHAT IS THIS + self._set_register(_CANINTE, _RX0IF | _RX1IF) + sleep(0.010) + self._mod_register( + _RXB0CTRL, + _RXB_RX_MASK | _RXB_BUKT_MASK, + _RXB_RX_STDEXT | _RXB_BUKT_MASK, + ) + + self._mod_register(_RXB1CTRL, _RXB_RX_MASK, _RXB_RX_STDEXT) + if self.loopback: + new_mode = _MODE_LOOPBACK + elif self.silent: + new_mode = _MODE_LISTENONLY + else: + new_mode = _MODE_NORMAL + + self._set_mode(new_mode) + + def sleep(self): + """Put the MCP2515 to sleep""" + self._set_mode(_MODE_SLEEP) + + def send(self, message_obj): + """Send a message on the bus with the given data and id. If the message could not be sent + due to a full fifo or a bus error condition, RuntimeError is raised. + + Args: + message (canio.Message): The message to send. Must be a valid `canio.Message` + """ + + # TODO: Timeout + tx_buff = self._get_tx_buffer() # info = addr. + if tx_buff is None: + raise RuntimeError("No transmit buffer available to send") + + return self._write_message(tx_buff, message_obj) + + @property + def unread_message_count(self): + """The number of messages that have been received but not read with `read_message` + + Returns: + int: The unread message count + """ + self._read_from_rx_buffers() + + return len(self._unread_message_queue) + + def read_message(self): + """Read the next available message + + Returns: + `canio.Message`: The next available message or None if one is not available + """ + if self.unread_message_count == 0: + return None + + return self._unread_message_queue.pop(0) + + def _read_rx_buffer(self, read_command): + for i in range(len(self._buffer)): # pylint: disable=consider-using-enumerate + self._buffer[i] = 0 + + # read from buffer + with self._bus_device_obj as spi: + self._buffer[0] = read_command + spi.write_readinto( + self._buffer, # because the reference does similar + self._buffer, + out_start=0, + out_end=1, + in_start=0, + in_end=1, + ) + + spi.readinto(self._buffer, end=15) + ######### Unpack IDs/ set Extended ####### + + raw_ids = unpack_from(">I", self._buffer)[0] + extended, sender_id = self._unload_ids(raw_ids) + ############# Length/RTR Size ######### + dlc = self._buffer[4] + # length is max 8 + message_length = min(8, dlc & 0xF) + + if (dlc & _RTR_MASK) > 0: + frame_obj = RemoteTransmissionRequest( + sender_id, message_length, extended=extended + ) + else: + frame_obj = Message( + sender_id, + data=bytes(self._buffer[5 : 5 + message_length]), + extended=extended, + ) + self._unread_message_queue.append(frame_obj) + + def _read_from_rx_buffers(self): + """Read the next available message into the given `bytearray` + + Args: + msg_buffer (bytearray): The buffer to load the message into + """ + status = self._read_status() + + # TODO: read and store all available messages + if status & 0b1: + self._read_rx_buffer(_READ_RX0) + + if status & 0b10: + self._read_rx_buffer(_READ_RX1) + + def _write_message(self, tx_buffer, message_obj): + + if tx_buffer is None: + raise RuntimeError("No transmit buffer available to send") + if isinstance(message_obj, RemoteTransmissionRequest): + dlc = message_obj.length + else: + dlc = len(message_obj.data) + + if dlc > _MAX_CAN_MSG_LEN: + raise AttributeError("Message/RTR length must be <=%d" % _MAX_CAN_MSG_LEN) + load_command = tx_buffer.LOAD_CMD + + if isinstance(message_obj, RemoteTransmissionRequest): + dlc |= _RTR_MASK + + # get id buffer segment + + self._load_id_buffer(message_obj.id, message_obj.extended) + + # this splits up the id header, dlc (len, rtr status), and message buffer + # TODO: check if we can send in one buffer, in which case `id_buffer` isn't needed + + with self._bus_device_obj as spi: + # send write command for the given buffer + self._buffer[0] = load_command + # spi.write(self._buffer, end=1) + spi.write_readinto( + self._buffer, # because the reference does similar + self._buffer, + out_start=0, + out_end=1, + in_start=0, + in_end=1, + ) + + # send id bytes + spi.write(self._id_buffer, end=4) + + # send DLC + + spi.write(bytearray([dlc])) + # send message bytes, limit to 8? + if isinstance(message_obj, Message): + spi.write(message_obj.data, end=8) + + # send the frame based on the current buffers + self._start_transmit(tx_buffer) + return True + + # TODO: Priority + def _start_transmit(self, tx_buffer): + # + self._buffer[0] = tx_buffer.SEND_CMD + with self._bus_device_obj as spi: + spi.write_readinto( + self._buffer, # because the reference does similar + self._buffer, + out_start=0, + out_end=1, + in_start=0, + in_end=1, + ) + + def _set_filter_register(self, filter_index, mask, extended): + filter_reg_addr = FILTERS[filter_index] + self._write_id_to_register(filter_reg_addr, mask, extended) + + def _set_mask_register(self, mask_index, mask, extended): + mask_reg_addr = MASKS[mask_index] + self._write_id_to_register(mask_reg_addr, mask, extended) + + @staticmethod + def _unload_ids(raw_ids): + """In=> 32-bit int packed with (StdID or ExTID top11 + bot18)+ extid bit + out=> id, extended flag""" + extended = (raw_ids & _TXB_EXIDE_M_16 << 16) > 0 + # std id field is most significant 11 bits of 4 bytes of id registers + top_chunk = raw_ids & EXTID_TOP_11_READ_MASK + if extended: + # get bottom 18 + bottom_chunk = raw_ids & EXTID_BOTTOM_18_MASK + # shift the top chunk back down 3 to start/end at bit 28=29th + top_chunk >>= 3 + sender_id = top_chunk | bottom_chunk + else: + # shift down the 3 [res+extid+res]+18 extid bits + sender_id = top_chunk >> (18 + 3) + return (extended, sender_id) + + def _load_id_buffer(self, can_id, extended=False): + self._id_buffer[0] = 0 + self._id_buffer[1] = 0 + self._id_buffer[2] = 0 + self._id_buffer[3] = 0 + + if extended: + extended_id = can_id + # mask off top 11 + high_11 = extended_id & EXTID_TOP_11_WRITE_MASK + # mask off bottom 18 + low_18 = extended_id & EXTID_BOTTOM_18_MASK + # shift up high piece to fill MSBits and make space for extended flag + high_11 <<= 3 + # or 'em together! + extended_id_shifted = high_11 | low_18 + final_id = extended_id_shifted | EXTID_FLAG_MASK + # set dat FLAG + + else: + std_id = can_id & STDID_BOTTOM_11_MASK # The actual ID? + # shift up to fit all 4 bytes + final_id = std_id << (16 + 5) + + # top = (final_id & EXTID_TOP_11_READ_MASK) >> 21 + # flags = (final_id & (0x7 << 18)) >> 18 + # bottom = final_id & EXTID_BOTTOM_18_MASK + # print( + # "final final_id: 0b{top:011b} {flags:03b} {bottom:018b}".format( + # top=top, flags=flags, bottom=bottom + # ) + # ) + pack_into(">I", self._id_buffer, 0, final_id) + + def _write_id_to_register(self, register, can_id, extended=False): + # load register in to ID buffer + + current_mode = self._mode + self._set_mode(_MODE_CONFIG) + # set the mask in the ID buffer + + self._load_id_buffer(can_id, extended) + + # write with buffer + with self._bus_device_obj as spi: + # send write command for the given bufferf + self._buffer[0] = _WRITE + self._buffer[1] = register + # spi.write(self._buffer, end=1) + spi.write_readinto( + self._buffer, # because the reference does similar + self._buffer, + out_start=0, + out_end=2, + in_start=0, + in_end=2, + ) + + # send id bytes + spi.write(self._id_buffer, end=4) + + self._set_mode(current_mode) + + @property + def _tx_buffers_in_use(self): + # the ref code allows for reserving buffers, but didn't see any way + # to use them. maybe un-reserve then use? + # TODO: this should return a tuple of busy states + # byte status = mcp2515_readStatus() & MCP_STAT_TX_PENDING_MASK + status = self._read_status() + self._dbg("Status byte:", "{:#010b}".format(status)) + return ( + bool(status & _STAT_TX0_PENDING), + bool(status & _STAT_TX1_PENDING), + bool(status & _STAT_TX2_PENDING), + ) + + def _get_tx_buffer(self): + """Get the address of the next available tx buffer and unset + its interrupt bit in _CANINTF""" + # check all buffers by looking for match on + txs_busy = self._tx_buffers_in_use + if all(txs_busy): + self._dbg("none available!") + return None + buffer_index = txs_busy.index(False) # => self._tx_buffers + tx_buffer = self._tx_buffers[buffer_index] + + self._mod_register(_CANINTF, tx_buffer.INT_FLAG_MASK, 0) + return tx_buffer + + def _set_baud_rate(self): + # ******* set baud rate *********** + if self._crystal_freq not in _BAUD_RATES: + raise ValueError( + f"Incorrect crystal frequency - must be one of: {tuple(_BAUD_RATES.keys())}" + ) + + cnf1, cnf2, cnf3 = _BAUD_RATES[self._crystal_freq][self.baudrate] + + self._set_register(_CNF1, cnf1) + self._set_register(_CNF2, cnf2) + self._set_register(_CNF3, cnf3) + sleep(0.010) + + def _reset(self): + self._buffer[0] = _RESET + with self._bus_device_obj as spi: + spi.write(self._buffer, end=1) + sleep(0.010) + + def _set_mode(self, mode): + stat_reg = self._read_register(_CANSTAT) + current_mode = stat_reg & _MODE_MASK + + if current_mode == mode: + return + self._timer.rewind_to(5) + while not self._timer.expired: + + new_mode_set = self._request_new_mode(mode) + if new_mode_set: + self._mode = mode + return + + raise RuntimeError("Unable to change mode") + + def _request_new_mode(self, mode): + self._timer.rewind_to(0.200) + while not self._timer.expired: + # Request new mode + # This is inside the loop as sometimes requesting the new mode once doesn't work + # (usually when attempting to sleep) + self._mod_register(_CANCTRL, _MODE_MASK, mode) + + status = self._read_register(_CANSTAT) + if (status & _MODE_MASK) == mode: + return True + + raise RuntimeError("Timeout setting Mode") + + def _mod_register(self, register_addr, mask, new_value): + """There appears to be an interface on the MCP2515 that allows for + setting a register using a mask""" + self._buffer[0] = _BITMOD + self._buffer[1] = register_addr + self._buffer[2] = mask + self._buffer[3] = new_value + with self._bus_device_obj as spi: + spi.write(self._buffer, end=4) + + def _read_register(self, regsiter_addr): + self._buffer[0] = _READ + self._buffer[1] = regsiter_addr + + with self._bus_device_obj as spi: + spi.write(self._buffer, end=2) + self._buffer[0] = 0 + spi.write_readinto( + self._buffer, self._buffer, out_start=0, out_end=1, in_start=0, in_end=1 + ) + + return self._buffer[0] + + def _read_status(self): + self._buffer[0] = _READ_STATUS + with self._bus_device_obj as spi: + spi.write(self._buffer, end=1) + spi.readinto(self._buffer, start=0, end=1) + return self._buffer[0] + + def _set_register(self, regsiter_addr, register_value): + self._buffer[0] = _WRITE + self._buffer[1] = regsiter_addr + self._buffer[2] = register_value + with self._bus_device_obj as spi: + spi.write(self._buffer, end=3) + + def _get_bus_status(self): + """Get the status flags that report the state of the bus""" + bus_flags = self._read_register(_EFLG) + + flags = [] + for idx in range(8): + bit_mask = 1 << idx + flags.append((bus_flags & bit_mask) > 0) + ( # pylint:disable=unbalanced-tuple-unpacking + error_warn, + _rx_error_warn, + _tx_err_warn, + rx_error_passive, + tx_error_passive, + buss_off, + self._rx0_overflow, + self._rx1_overflow, + ) = flags + if self._rx0_overflow or self._rx0_overflow: + self._mod_register( + _EFLG, 0xC0, 0 + ) # clear overflow bits now that we've recorded them + + if buss_off: + self._bus_state = BusState.BUS_OFF + elif tx_error_passive or rx_error_passive: + self._bus_state = BusState.ERROR_PASSIVE + elif error_warn: + self._bus_state = BusState.ERROR_WARNING + else: + self._bus_state = BusState.ERROR_ACTIVE + + def _create_mask(self, match): + mask = match.mask + if mask == 0: + if match.extended: + mask = EXTID_BOTTOM_29_MASK + else: + mask = STDID_BOTTOM_11_MASK + + masks_used = len(self._masks_in_use) + if masks_used < len(MASKS): + next_mask_index = masks_used + + self._set_mask_register(next_mask_index, mask, match.extended) + self._masks_in_use.append(MASKS[next_mask_index]) + return next_mask_index + + raise RuntimeError("No Masks Available") + + def _create_filter(self, match, mask_index): + + next_filter_index = len(self._filters_in_use[mask_index]) + if next_filter_index == len(FILTERS[mask_index]): + raise RuntimeError("No Filters Available") + + filter_register = FILTERS[mask_index][next_filter_index] + + self._write_id_to_register(filter_register, match.address, match.extended) + self._filters_in_use[mask_index].append(filter_register) + + def deinit_filtering_registers(self): + """Clears the Receive Mask and Filter Registers""" + + for mask_index, mask_reg in enumerate(MASKS): + self._set_register(mask_reg, 0) + + for filter_reg in FILTERS[mask_index]: + self._set_register(filter_reg, 0) + self._masks_in_use = [] + self._filters_in_use = [[], []] + + ######## CANIO API METHODS ############# + @property + def baudrate(self): + """The baud rate (read-only)""" + return self._baudrate + + @property + def transmit_error_count(self): + """ The number of transmit errors (read-only). Increased for a detected transmission error,\ + decreased for successful transmission. Limited to the range from 0 to 255 inclusive. \ + Also called TEC.""" + return self._read_register(_TEC) + + @property + def receive_error_count(self): + """ The number of receive errors (read-only). Increased for a detected reception error, \ + decreased for successful reception. Limited to the range from 0 to 255 inclusive. Also + called REC.""" + return self._read_register(_REC) + + @property + def error_warning_state_count(self): + """Not supported by hardware. Raises an `AttributeError` if called""" + raise AttributeError("`error_warning_state_count` not supported by hardware") + + @property + def error_passive_state_count(self): + """Not supported by hardware. Raises an `AttributeError` if called""" + raise AttributeError("`error_passive_state_count` not supported by hardware") + + @property + def bus_off_state_count(self): + """Not supported by hardware. Raises an `AttributeError` if called""" + raise AttributeError("`bus_off_state_count` not supported by hardware") + + @property + def state(self): # State + """The current state of the bus. (read-only)""" + self._get_bus_status() + return self._bus_state + + @property + def loopback(self): # bool + """True if the device was created in loopback mode, False otherwise. (read-only)""" + return self._loopback + + @property + def silent(self): # bool + """True if the device was created in silent mode, False otherwise. (read-only)""" + return self._silent + + def restart(self): + """If the device is in the bus off state, restart it.""" + self.initialize() + + def listen(self, matches=None, *, timeout: float = 10): + """Start receiving messages that match any one of the filters. + + Creating a listener is an expensive operation and can interfere with reception of messages + by other listeners. + + There is an implementation-defined maximum number of listeners and limit to the complexity of + the filters. + + If the hardware cannot support all the requested matches, a ValueError is raised. Note that \ + generally there are some number of hardware filters shared among all fifos. + + A message can be received by at most one Listener. If more than one listener matches a message,\ + it is undefined which one actually receives it. + + An empty filter list causes all messages to be accepted. + + Timeout dictates how long ``receive()`` will block. + + Args: + match (Optional[Sequence[Match]], optional): [description]. Defaults to None. + timeout (float, optional): [description]. Defaults to 10. + + Returns: + Listener: [description] + """ + if matches is None: + matches = [] + elif self.silent and not self.loopback: + raise AttributeError( + "Hardware does not support setting `matches` in when\ + `silent`==`True` and `loopback` == `False`" + ) + + for match in matches: + self._dbg("match:", match) + mask_index_used = self._create_mask(match) + self._create_filter(match, mask_index=mask_index_used) + + used_masks = len(self._masks_in_use) + # if matches were made and there are unused masks + # set the unused masks to prevent them from leaking packets + if len(matches) > 0 and used_masks < len(MASKS): + next_mask_index = used_masks + for idx in range(next_mask_index, len(MASKS)): + print("using unused mask index:", idx) + self._create_mask(matches[-1]) + + return Listener(self, timeout) + + def deinit(self): + """Deinitialize this object, freeing its hardware resources""" + self._cs_pin.deinit() + + def __enter__(self): + """Returns self, to allow the object to be used in a The with statement statement for \ + resource control""" + return self + + def __exit__(self, unused1, unused2, unused3): + """Calls deinit()""" + self.deinit() + + ##################### End canio API ################ + + def _dbg(self, *args, **kwargs): + if self._debug: + print("DBG::\t\t", *args, **kwargs) diff --git a/FC_Board/lib/adafruit_mcp2515/canio/__init__.py b/FC_Board/lib/adafruit_mcp2515/canio/__init__.py new file mode 100644 index 00000000..41ff222a --- /dev/null +++ b/FC_Board/lib/adafruit_mcp2515/canio/__init__.py @@ -0,0 +1,220 @@ +# SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries +# +# SPDX-License-Identifier: MIT +"""Python implementation of the CircuitPython core `canio` API""" +# pylint:disable=too-few-public-methods, invalid-name, redefined-builtin +import time +from ..timer import Timer + + +class Message: + """A class representing a CANbus data frame + + :param int id: The numeric ID of the message + :param bytes data: The content of the message, from 0 to 8 bytes of data + :param bool extended: True if the message has an extended identifier, + False if it has a standard identifier + """ + + # pylint:disable=too-many-arguments,invalid-name,redefined-builtin + def __init__(self, id, data, extended=False): + self._data = None + self.id = id + self.data = data + self.extended = extended + + id: int + """The numeric ID of the message""" + + extended: bool + """Indicates whether the the message has an extended identifier""" + + @property + def data(self): + """The content of the message""" + return self._data + + @data.setter + def data(self, new_data): + if (new_data is None) or (not (type(new_data) in [bytes, bytearray])): + + raise AttributeError( + "non-RTR canio.Message must have a `data` argument of type `bytes`" + ) + if len(new_data) > 8: + raise AttributeError( + "`canio.Message` object data must be of length 8 or less" + ) + # self.rtr = False + # self._data = new_data + self._data = bytearray(new_data) + + +class RemoteTransmissionRequest: + """A class representing a CANbus remote frame + + :param int id: The numeric ID of the message + :param length int: The length of the requested message + :param bool extended: True if the message has an extended identifier, + False if it has a standard identifier + """ + + def __init__(self, id: int, length: int, *, extended: bool = False): + self.id = id + self.length = length + self.extended = extended + + id: int + """The numeric ID of the message""" + + extended: bool + """Indicates whether the the message has an extended identifier""" + + length: int + """The length of the requested message, from 0 to 8""" + + +# Replace the above implementation with core canio implementation if it is available +try: + from canio import Message, RemoteTransmissionRequest +except ImportError: + pass + + +class Listener: + """Listens for a CAN message + + canio.Listener is not constructed directly, but instead by calling the + ``listen`` method of a canio.CAN object. + """ + + def __init__(self, can_bus_obj, timeout=1.0): + self._timer = Timer() + self._can_bus_obj = can_bus_obj + self._timeout = None + self.timeout = timeout + + @property + def timeout(self): + """The maximum amount of time in seconds that ``read`` or ``readinto`` + will wait before giving up""" + return self._timeout + + @timeout.setter + def timeout(self, timeout): + self._timeout = float(timeout) + + def receive(self): + """Receives a message. If after waiting up to self.timeout seconds if no message is\ + received, None is returned. Otherwise, a Message is returned.""" + if self._can_bus_obj is None: + raise ValueError( + "Object has been deinitialized and can no longer be used. Create a new object." + ) + self._timer.rewind_to(self.timeout) + while not self._timer.expired: + if self._can_bus_obj.unread_message_count == 0: + continue + return self._can_bus_obj.read_message() + return None + + def in_waiting(self): + """Returns the number of messages waiting""" + if self._can_bus_obj is None: + raise ValueError( + "Object has been deinitialized and can no longer be used. Create a new object." + ) + return self._can_bus_obj.unread_message_count + + def __iter__(self): + """Returns self""" + if self._can_bus_obj is None: + raise ValueError( + "Object has been deinitialized and can no longer be used. Create a new object." + ) + return self + + def __next__(self): + """Receives a message, after waiting up to self.timeout seconds""" + if self._can_bus_obj is None: + raise ValueError( + "Object has been deinitialized and can no longer be used. Create a new object." + ) + return self.receive() + + def deinit(self): + """Deinitialize this object, freeing its hardware resources""" + self._can_bus_obj.deinit_filtering_registers() + self._timer = None + self._can_bus_obj = None + self._timeout = None + + def __enter__(self): + """Returns self, to allow the object to be used in a The with statement statement for\ + resource control""" + if self._can_bus_obj is None: + raise ValueError( + "Object has been deinitialized and can no longer be used. Create a new object." + ) + return self + + def __exit__(self, unused1, unused2, unused3): + """Calls deinit()""" + if not self._can_bus_obj: + raise ValueError( + "Object has been deinitialized and can no longer be used. Create a new object." + ) + self.deinit() + + +class BusState: + """The state of the CAN bus""" + + ERROR_ACTIVE = 0 + """The bus is in the normal (active) state""" + + ERROR_WARNING = 1 + """ The bus is in the normal (active) state, but a moderate number of\ + errors have occurred recently. + + NOTE: Not all implementations may use ERROR_WARNING. Do not rely on seeing ERROR_WARNING\ + before ERROR_PASSIVE. + """ + + ERROR_PASSIVE = 2 + """ The bus is in the passive state due to the number of errors that have occurred recently. + + This device will acknowledge packets it receives, but cannot transmit messages. If additional\ + errors occur, this device may progress to BUS_OFF. If it successfully acknowledges other\ + packets on the bus, it can return to ERROR_WARNING or ERROR_ACTIVE and transmit packets. + """ + BUS_OFF = 3 + """ The bus has turned off due to the number of errors that have occurred recently. It must be \ + restarted before it will send or receive packets. This device will neither send or acknowledge \ + packets on the bus.""" + + +class Match: + """A class representing an ID pattern to match against""" + + def __init__(self, address: int, *, mask: int = 0, extended: bool = False): + """Describe CAN bus messages to match + + Construct a Match with the given properties. + + If mask is nonzero, then the filter is for any sender which matches all the nonzero bits in\ + mask. Otherwise, it matches exactly the given address. If extended is true then only\ + extended addresses are matched, otherwise only standard addresses are matched. + + Args: + address (int): he address to match + mask (int, optional): The optional mask of addresses to match. Defaults to 0. + extended (bool, optional): True to match extended addresses, False to match standard\ + addresses. + + Returns: + [type]: [description] + """ + self.address = address + self.mask = mask + self.extended = extended diff --git a/FC_Board/lib/adafruit_mcp2515/timer.py b/FC_Board/lib/adafruit_mcp2515/timer.py new file mode 100644 index 00000000..814fb0c6 --- /dev/null +++ b/FC_Board/lib/adafruit_mcp2515/timer.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2020 Bryan Siepert for Adafruit Industries +# +# SPDX-License-Identifier: MIT +"""Provides a simple timer class; see `Timer`""" +from time import monotonic + + +class Timer: + """A reusable class to track timeouts, like an egg timer""" + + def __init__(self, timeout=0.0): + self._timeout = None + self._start_time = None + if timeout: + self.rewind_to(timeout) + + @property + def expired(self): + """Returns the expiration status of the timer + + Returns: + bool: True if more than `timeout` seconds has past since it was set + """ + return (monotonic() - self._start_time) > self._timeout + + def rewind_to(self, new_timeout): + """Re-wind the timer to a new timeout and start ticking""" + self._timeout = float(new_timeout) + self._start_time = monotonic() diff --git a/FC_Board/lib/adafruit_ov5640/__init__.py b/FC_Board/lib/adafruit_ov5640/__init__.py new file mode 100644 index 00000000..3c6a6659 --- /dev/null +++ b/FC_Board/lib/adafruit_ov5640/__init__.py @@ -0,0 +1,1583 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_ov5640` +================================================================================ + +CircuitPython driver for OV5640 Camera + + +* Author(s): Jeff Epler + +Implementation Notes +-------------------- + +**Hardware:** + +* ESP32-S2 Kaluga Dev Kit featuring ESP32-S2 WROVER + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +""" + +# pylint: disable=too-many-lines +# pylint: disable=too-many-public-methods +# imports +import time +import imagecapture +import pwmio +from adafruit_bus_device.i2c_device import I2CDevice + +try: + from typing import Optional, Sequence, List, Union + from busio import I2C + from microcontroller import Pin + from digitalio import DigitalInOut +except ImportError: + pass + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ov5640.git" + +from micropython import const + +OV5640_COLOR_RGB = 0 +OV5640_COLOR_YUV = 1 +OV5640_COLOR_GRAYSCALE = 2 +OV5640_COLOR_JPEG = 3 + +# fmt: off + +_SYSTEM_RESET00 = const(0x3000) # Reset for Individual Block +# (0: enable block; 1: reset block) +# Bit[7]: Reset BIST +# Bit[6]: Reset MCU program memory +# Bit[5]: Reset MCU +# Bit[4]: Reset OTP +# Bit[3]: Reset STB +# Bit[2]: Reset d5060 +# Bit[1]: Reset timing control +# Bit[0]: Reset array control + +_SYSTEM_RESET02 = const(0x3002) # Reset for Individual Block +# (0: enable block; 1: reset block) +# Bit[7]: Reset VFIFO +# Bit[5]: Reset format +# Bit[4]: Reset JFIFO +# Bit[3]: Reset SFIFO +# Bit[2]: Reset JPG +# Bit[1]: Reset format MUX +# Bit[0]: Reset average + +_CLOCK_ENABLE02 = const(0x3006) # Clock Enable Control +# (0: disable clock; 1: enable clock) +# Bit[7]: Enable PSRAM clock +# Bit[6]: Enable FMT clock +# Bit[5]: Enable JPEG 2x clock +# Bit[3]: Enable JPEG clock +# Bit[1]: Enable format MUX clock +# Bit[0]: Enable average clock + +_SYSTEM_CTROL0 = const(0x3008) +# Bit[7]: Software reset +# Bit[6]: Software power down +# Bit[5]: Reserved +# Bit[4]: SRB clock SYNC enable +# Bit[3]: Isolation suspend select +# Bit[2:0]: Not used + +_CHIP_ID_HIGH = const(0x300A) + +_DRIVE_CAPABILITY = const(0x302C) +# Bit[7:6]: +# 00: 1x +# 01: 2x +# 10: 3x +# 11: 4x + +_SC_PLLS_CTRL0 = const(0x303A) +# Bit[7]: PLLS bypass +_SC_PLLS_CTRL1 = const(0x303B) +# Bit[4:0]: PLLS multiplier +_SC_PLLS_CTRL2 = const(0x303C) +# Bit[6:4]: PLLS charge pump control +# Bit[3:0]: PLLS system divider +_SC_PLLS_CTRL3 = const(0x303D) +# Bit[5:4]: PLLS pre-divider +# 00: 1 +# 01: 1.5 +# 10: 2 +# 11: 3 +# Bit[2]: PLLS root-divider - 1 +# Bit[1:0]: PLLS seld5 +# 00: 1 +# 01: 1 +# 10: 2 +# 11: 2.5 + +# AEC/AGC control functions +_AEC_PK_MANUAL = const(0x3503) +# AEC Manual Mode Control +# Bit[7:6]: Reserved +# Bit[5]: Gain delay option +# Valid when 0x3503[4]=1’b0 +# 0: Delay one frame latch +# 1: One frame latch +# Bit[4:2]: Reserved +# Bit[1]: AGC manual +# 0: Auto enable +# 1: Manual enable +# Bit[0]: AEC manual +# 0: Auto enable +# 1: Manual enable + +# gain = {0x350A[1:0], 0x350B[7:0]} / 16 + + +_X_ADDR_ST_H = const(0x3800) +# Bit[3:0]: X address start[11:8] +_X_ADDR_ST_L = const(0x3801) +# Bit[7:0]: X address start[7:0] +_Y_ADDR_ST_H = const(0x3802) +# Bit[2:0]: Y address start[10:8] +_Y_ADDR_ST_L = const(0x3803) +# Bit[7:0]: Y address start[7:0] +_X_ADDR_END_H = const(0x3804) +# Bit[3:0]: X address end[11:8] +_X_ADDR_END_L = const(0x3805) +# Bit[7:0]: +_Y_ADDR_END_H = const(0x3806) +# Bit[2:0]: Y address end[10:8] +_Y_ADDR_END_L = const(0x3807) +# Bit[7:0]: +# Size after scaling +_X_OUTPUT_SIZE_H = const(0x3808) +# Bit[3:0]: DVP output horizontal width[11:8] +_X_OUTPUT_SIZE_L = const(0x3809) +# Bit[7:0]: +_Y_OUTPUT_SIZE_H = const(0x380A) +# Bit[2:0]: DVP output vertical height[10:8] +_Y_OUTPUT_SIZE_L = const(0x380B) +# Bit[7:0]: +_X_TOTAL_SIZE_H = const(0x380C) +# Bit[3:0]: Total horizontal size[11:8] +_X_TOTAL_SIZE_L = const(0x380D) +# Bit[7:0]: +_Y_TOTAL_SIZE_H = const(0x380E) +# Bit[7:0]: Total vertical size[15:8] +_Y_TOTAL_SIZE_L = const(0x380F) +# Bit[7:0]: +_X_OFFSET_H = const(0x3810) +# Bit[3:0]: ISP horizontal offset[11:8] +_X_OFFSET_L = const(0x3811) +# Bit[7:0]: +_Y_OFFSET_H = const(0x3812) +# Bit[2:0]: ISP vertical offset[10:8] +_Y_OFFSET_L = const(0x3813) +# Bit[7:0]: +_X_INCREMENT = const(0x3814) +# Bit[7:4]: Horizontal odd subsample increment +# Bit[3:0]: Horizontal even subsample increment +_Y_INCREMENT = const(0x3815) +# Bit[7:4]: Vertical odd subsample increment +# Bit[3:0]: Vertical even subsample increment +# Size before scaling +# X_INPUT_SIZE = const( (X_ADDR_END - X_ADDR_ST + 1 - (2 * X_OFFSET))) +# Y_INPUT_SIZE = const( (Y_ADDR_END - Y_ADDR_ST + 1 - (2 * Y_OFFSET))) + +# mirror and flip registers +_TIMING_TC_REG20 = const(0x3820) +# Timing Control Register +# Bit[2:1]: Vertical flip enable +# 00: Normal +# 11: Vertical flip +# Bit[0]: Vertical binning enable +_TIMING_TC_REG21 = const(0x3821) +# Timing Control Register +# Bit[5]: Compression Enable +# Bit[2:1]: Horizontal mirror enable +# 00: Normal +# 11: Horizontal mirror +# Bit[0]: Horizontal binning enable + +_PCLK_RATIO = const(0x3824) +# Bit[4:0]: PCLK ratio manual + +# frame control registers +_FRAME_CTRL01 = const( + 0x4201 +) +# Control Passed Frame Number When both ON and OFF number set to 0x00,frame +# control is in bypass mode +# Bit[7:4]: Not used +# Bit[3:0]: Frame ON number +_FRAME_CTRL02 = const( + 0x4202 +) +# Control Masked Frame Number When both ON and OFF number set to 0x00,frame +# control is in bypass mode +# Bit[7:4]: Not used +# BIT[3:0]: Frame OFF number + +# format control registers +_FORMAT_CTRL00 = const(0x4300) + +_CLOCK_POL_CONTROL = const(0x4740) +# Bit[5]: PCLK polarity 0: active low +# 1: active high +# Bit[3]: Gate PCLK under VSYNC +# Bit[2]: Gate PCLK under HREF +# Bit[1]: HREF polarity +# 0: active low +# 1: active high +# Bit[0] VSYNC polarity +# 0: active low +# 1: active high + +_ISP_CONTROL_01 = const(0x5001) +# Bit[5]: Scale enable +# 0: Disable +# 1: Enable + +# output format control registers +_FORMAT_CTRL = const(0x501F) +# Format select +# Bit[2:0]: +# 000: YUV422 +# 001: RGB +# 010: Dither +# 011: RAW after DPC +# 101: RAW after CIP + +# ISP top control registers +_PRE_ISP_TEST_SETTING_1 = const(0x503D) +# Bit[7]: Test enable +# 0: Test disable +# 1: Color bar enable +# Bit[6]: Rolling +# Bit[5]: Transparent +# Bit[4]: Square black and white +# Bit[3:2]: Color bar style +# 00: Standard 8 color bar +# 01: Gradual change at vertical mode 1 +# 10: Gradual change at horizontal +# 11: Gradual change at vertical mode 2 +# Bit[1:0]: Test select +# 00: Color bar +# 01: Random data +# 10: Square data +# 11: Black image + +# exposure = {0x3500[3:0], 0x3501[7:0], 0x3502[7:0]} / 16 × tROW + +_SCALE_CTRL_1 = const(0x5601) +# Bit[6:4]: HDIV RW +# DCW scale times +# 000: DCW 1 time +# 001: DCW 2 times +# 010: DCW 4 times +# 100: DCW 8 times +# 101: DCW 16 times +# Others: DCW 16 times +# Bit[2:0]: VDIV RW +# DCW scale times +# 000: DCW 1 time +# 001: DCW 2 times +# 010: DCW 4 times +# 100: DCW 8 times +# 101: DCW 16 times +# Others: DCW 16 times + +_SCALE_CTRL_2 = const(0x5602) +# X_SCALE High Bits +_SCALE_CTRL_3 = const(0x5603) +# X_SCALE Low Bits +_SCALE_CTRL_4 = const(0x5604) +# Y_SCALE High Bits +_SCALE_CTRL_5 = const(0x5605) +# Y_SCALE Low Bits +_SCALE_CTRL_6 = const(0x5606) +# Bit[3:0]: V Offset + +_VFIFO_CTRL0C = const(0x460C) +# Bit[1]: PCLK manual enable +# 0: Auto +# 1: Manual by PCLK_RATIO + +_VFIFO_X_SIZE_H = const(0x4602) +_VFIFO_X_SIZE_L = const(0x4603) +_VFIFO_Y_SIZE_H = const(0x4604) +_VFIFO_Y_SIZE_L = const(0x4605) + +_COMPRESSION_CTRL00 = const(0x4400) +_COMPRESSION_CTRL01 = const(0x4401) +_COMPRESSION_CTRL02 = const(0x4402) +_COMPRESSION_CTRL03 = const(0x4403) +_COMPRESSION_CTRL04 = const(0x4404) +_COMPRESSION_CTRL05 = const(0x4405) +_COMPRESSION_CTRL06 = const(0x4406) +_COMPRESSION_CTRL07 = const(0x4407) +# Bit[5:0]: QS +_COMPRESSION_ISI_CTRL = const(0x4408) +_COMPRESSION_CTRL09 = const(0x4409) +_COMPRESSION_CTRL0A = const(0x440A) +_COMPRESSION_CTRL0B = const(0x440B) +_COMPRESSION_CTRL0C = const(0x440C) +_COMPRESSION_CTRL0D = const(0x440D) +_COMPRESSION_CTRL0E = const(0x440E) + +_TEST_COLOR_BAR = const(0xC0) +# Enable Color Bar roling Test + +_AEC_PK_MANUAL_AGC_MANUALEN = const(0x02) +# Enable AGC Manual enable +_AEC_PK_MANUAL_AEC_MANUALEN = const(0x01) +# Enable AEC Manual enable + +_TIMING_TC_REG20_VFLIP = const(0x06) +# Vertical flip enable +_TIMING_TC_REG21_HMIRROR = const(0x06) +# Horizontal mirror enable + +OV5640_SIZE_96X96 = 0 # 96x96 +OV5640_SIZE_QQVGA = 1 # 160x120 +OV5640_SIZE_QCIF = 2 # 176x144 +OV5640_SIZE_HQVGA = 3 # 240x176 +OV5640_SIZE_240X240 = 4 # 240x240 +OV5640_SIZE_QVGA = 5 # 320x240 +OV5640_SIZE_CIF = 6 # 400x296 +OV5640_SIZE_HVGA = 7 # 480x320 +OV5640_SIZE_VGA = 8 # 640x480 +OV5640_SIZE_SVGA = 9 # 800x600 +OV5640_SIZE_XGA = 10 # 1024x768 +OV5640_SIZE_HD = 11 # 1280x720 +OV5640_SIZE_SXGA = 12 # 1280x1024 +OV5640_SIZE_UXGA = 13 # 1600x1200 +OV5640_SIZE_QHDA = 14 # 2560x1440 +OV5640_SIZE_WQXGA = 15 # 2560x1600 +OV5640_SIZE_PFHD = 16 # 1088x1920 +OV5640_SIZE_QSXGA = 17 # 2560x1920 + +_ASPECT_RATIO_4X3 = const(0) +_ASPECT_RATIO_3X2 = const(1) +_ASPECT_RATIO_16X10 = const(2) +_ASPECT_RATIO_5X3 = const(3) +_ASPECT_RATIO_16X9 = const(4) +_ASPECT_RATIO_21X9 = const(5) +_ASPECT_RATIO_5X4 = const(6) +_ASPECT_RATIO_1X1 = const(7) +_ASPECT_RATIO_9X16 = const(8) + +_resolution_info = [ + [96, 96, _ASPECT_RATIO_1X1], # 96x96 + [160, 120, _ASPECT_RATIO_4X3], # QQVGA + [176, 144, _ASPECT_RATIO_5X4], # QCIF + [240, 176, _ASPECT_RATIO_4X3], # HQVGA + [240, 240, _ASPECT_RATIO_1X1], # 240x240 + [320, 240, _ASPECT_RATIO_4X3], # QVGA + [400, 296, _ASPECT_RATIO_4X3], # CIF + [480, 320, _ASPECT_RATIO_3X2], # HVGA + [640, 480, _ASPECT_RATIO_4X3], # VGA + [800, 600, _ASPECT_RATIO_4X3], # SVGA + [1024, 768, _ASPECT_RATIO_4X3], # XGA + [1280, 720, _ASPECT_RATIO_16X9], # HD + [1280, 1024, _ASPECT_RATIO_5X4], # SXGA + [1600, 1200, _ASPECT_RATIO_4X3], # UXGA + [2560, 1440, _ASPECT_RATIO_16X9], # QHD + [2560, 1600, _ASPECT_RATIO_16X10], # WQXGA + [1088, 1920, _ASPECT_RATIO_9X16], # Portrait FHD + [2560, 1920, _ASPECT_RATIO_4X3], # QSXGA + +] + + +_ratio_table = [ + # mw, mh, sx, sy, ex, ey, ox, oy, tx, ty + [2560, 1920, 0, 0, 2623, 1951, 32, 16, 2844, 1968], # 4x3 + [2560, 1704, 0, 110, 2623, 1843, 32, 16, 2844, 1752], # 3x2 + [2560, 1600, 0, 160, 2623, 1791, 32, 16, 2844, 1648], # 16x10 + [2560, 1536, 0, 192, 2623, 1759, 32, 16, 2844, 1584], # 5x3 + [2560, 1440, 0, 240, 2623, 1711, 32, 16, 2844, 1488], # 16x9 + [2560, 1080, 0, 420, 2623, 1531, 32, 16, 2844, 1128], # 21x9 + [2400, 1920, 80, 0, 2543, 1951, 32, 16, 2684, 1968], # 5x4 + [1920, 1920, 320, 0, 2543, 1951, 32, 16, 2684, 1968], # 1x1 + [1088, 1920, 736, 0, 1887, 1951, 32, 16, 1884, 1968], # 9x16 +] + +_pll_pre_div2x_factors = [1, 1, 2, 3, 4, 1.5, 6, 2.5, 8] +_pll_pclk_root_div_factors = [1,2,4,8] + +_REG_DLY = const(0xFFFF) +_REGLIST_TAIL = const(0x0000) + +_OV5640_STAT_FIRMWAREBAD = const(0x7F) +_OV5640_STAT_STARTUP = const(0x7E) +_OV5640_STAT_IDLE = const(0x70) +_OV5640_STAT_FOCUSING = const(0x00) +_OV5640_STAT_FOCUSED = const(0x10) + +_OV5640_CMD_TRIGGER_AUTOFOCUS = const(0x03) +_OV5640_CMD_AUTO_AUTOFOCUS = const(0x04) +_OV5640_CMD_RELEASE_FOCUS = const(0x08) +_OV5640_CMD_AF_SET_VCM_STEP = const(0x1A) +_OV5640_CMD_AF_GET_VCM_STEP = const(0x1B) + +_OV5640_CMD_MAIN = const(0x3022) +_OV5640_CMD_ACK = const(0x3023) +_OV5640_CMD_PARA0 = const(0x3024) +_OV5640_CMD_PARA1 = const(0x3025) +_OV5640_CMD_PARA2 = const(0x3026) +_OV5640_CMD_PARA3 = const(0x3027) +_OV5640_CMD_PARA4 = const(0x3028) +_OV5640_CMD_FW_STATUS = const(0x3029) + + +_sensor_default_regs = [ + _SYSTEM_CTROL0, 0x82, # software reset + _REG_DLY, 10, # delay 10ms + _SYSTEM_CTROL0, 0x42, # power down + # enable pll + 0x3103, 0x13, + # io direction + 0x3017, 0xFF, + 0x3018, 0xFF, + _DRIVE_CAPABILITY, 0xC3, + _CLOCK_POL_CONTROL, 0x21, + 0x4713, 0x02, # jpg mode select + _ISP_CONTROL_01, 0x83, # turn color matrix, awb and SDE + # sys reset + _SYSTEM_RESET00, 0x00, # enable all blocks + _SYSTEM_RESET02, 0x1C, # reset jfifo, sfifo, jpg, fmux, avg + # clock enable + 0x3004, 0xFF, + _CLOCK_ENABLE02, 0xC3, + # isp control + 0x5000, 0xA7, + _ISP_CONTROL_01, 0xA3, # +scaling? + 0x5003, 0x08, # special_effect + # unknown + 0x370C, 0x02, #!!IMPORTANT + 0x3634, 0x40, #!!IMPORTANT + # AEC/AGC + 0x3A02, 0x03, + 0x3A03, 0xD8, + 0x3A08, 0x01, + 0x3A09, 0x27, + 0x3A0A, 0x00, + 0x3A0B, 0xF6, + 0x3A0D, 0x04, + 0x3A0E, 0x03, + 0x3A0F, 0x30, # ae_level + 0x3A10, 0x28, # ae_level + 0x3A11, 0x60, # ae_level + 0x3A13, 0x43, + 0x3A14, 0x03, + 0x3A15, 0xD8, + 0x3A18, 0x00, # gainceiling + 0x3A19, 0xF8, # gainceiling + 0x3A1B, 0x30, # ae_level + 0x3A1E, 0x26, # ae_level + 0x3A1F, 0x14, # ae_level + # vcm debug + 0x3600, 0x08, + 0x3601, 0x33, + # 50/60Hz + 0x3C01, 0xA4, + 0x3C04, 0x28, + 0x3C05, 0x98, + 0x3C06, 0x00, + 0x3C07, 0x08, + 0x3C08, 0x00, + 0x3C09, 0x1C, + 0x3C0A, 0x9C, + 0x3C0B, 0x40, + 0x460C, 0x22, # disable jpeg footer + # BLC + 0x4001, 0x02, + 0x4004, 0x02, + # AWB + 0x5180, 0xFF, + 0x5181, 0xF2, + 0x5182, 0x00, + 0x5183, 0x14, + 0x5184, 0x25, + 0x5185, 0x24, + 0x5186, 0x09, + 0x5187, 0x09, + 0x5188, 0x09, + 0x5189, 0x75, + 0x518A, 0x54, + 0x518B, 0xE0, + 0x518C, 0xB2, + 0x518D, 0x42, + 0x518E, 0x3D, + 0x518F, 0x56, + 0x5190, 0x46, + 0x5191, 0xF8, + 0x5192, 0x04, + 0x5193, 0x70, + 0x5194, 0xF0, + 0x5195, 0xF0, + 0x5196, 0x03, + 0x5197, 0x01, + 0x5198, 0x04, + 0x5199, 0x12, + 0x519A, 0x04, + 0x519B, 0x00, + 0x519C, 0x06, + 0x519D, 0x82, + 0x519E, 0x38, + # color matrix (Saturation) + 0x5381, 0x1E, + 0x5382, 0x5B, + 0x5383, 0x08, + 0x5384, 0x0A, + 0x5385, 0x7E, + 0x5386, 0x88, + 0x5387, 0x7C, + 0x5388, 0x6C, + 0x5389, 0x10, + 0x538A, 0x01, + 0x538B, 0x98, + # CIP control (Sharpness) + 0x5300, 0x10, # sharpness + 0x5301, 0x10, # sharpness + 0x5302, 0x18, # sharpness + 0x5303, 0x19, # sharpness + 0x5304, 0x10, + 0x5305, 0x10, + 0x5306, 0x08, # denoise + 0x5307, 0x16, + 0x5308, 0x40, + 0x5309, 0x10, # sharpness + 0x530A, 0x10, # sharpness + 0x530B, 0x04, # sharpness + 0x530C, 0x06, # sharpness + # GAMMA + 0x5480, 0x01, + 0x5481, 0x00, + 0x5482, 0x1E, + 0x5483, 0x3B, + 0x5484, 0x58, + 0x5485, 0x66, + 0x5486, 0x71, + 0x5487, 0x7D, + 0x5488, 0x83, + 0x5489, 0x8F, + 0x548A, 0x98, + 0x548B, 0xA6, + 0x548C, 0xB8, + 0x548D, 0xCA, + 0x548E, 0xD7, + 0x548F, 0xE3, + 0x5490, 0x1D, + # Special Digital Effects (SDE) (UV adjust) + 0x5580, 0x06, # enable brightness and contrast + 0x5583, 0x40, # special_effect + 0x5584, 0x10, # special_effect + 0x5586, 0x20, # contrast + 0x5587, 0x00, # brightness + 0x5588, 0x00, # brightness + 0x5589, 0x10, + 0x558A, 0x00, + 0x558B, 0xF8, + 0x501D, 0x40, # enable manual offset of contrast + # power on + 0x3008, 0x02, + # 50Hz + 0x3C00, 0x04, + #_REG_DLY, 300, +] + + + +_reset_awb = [ + _ISP_CONTROL_01, 0x83, # turn color matrix, awb and SDE + # sys reset + _SYSTEM_RESET00, 0x00, # enable all blocks + _SYSTEM_RESET02, 0x1C, # reset jfifo, sfifo, jpg, fmux, avg + # clock enable + #0x3004, 0xFF, + #_CLOCK_ENABLE02, 0xC3, + # isp control + 0x5000, 0xA7, + _ISP_CONTROL_01, 0xA3, # +scaling? + 0x5003, 0x08, # special_effect + # unknown + 0x370C, 0x02, #!!IMPORTANT + 0x3634, 0x40, #!!IMPORTANT + # AEC/AGC + 0x3A02, 0x03, + 0x3A03, 0xD8, + 0x3A08, 0x01, + 0x3A09, 0x27, + 0x3A0A, 0x00, + 0x3A0B, 0xF6, + 0x3A0D, 0x04, + 0x3A0E, 0x03, + 0x3A0F, 0x30, # ae_level + 0x3A10, 0x28, # ae_level + 0x3A11, 0x60, # ae_level + 0x3A13, 0x43, + 0x3A14, 0x03, + 0x3A15, 0xD8, + 0x3A18, 0x00, # gainceiling + 0x3A19, 0xF8, # gainceiling + 0x3A1B, 0x30, # ae_level + 0x3A1E, 0x26, # ae_level + 0x3A1F, 0x14, # ae_level + # vcm debug + 0x3600, 0x08, + 0x3601, 0x33, + # 50/60Hz + 0x3C01, 0xA4, + 0x3C04, 0x28, + 0x3C05, 0x98, + 0x3C06, 0x00, + 0x3C07, 0x08, + 0x3C08, 0x00, + 0x3C09, 0x1C, + 0x3C0A, 0x9C, + 0x3C0B, 0x40, + 0x460C, 0x22, # disable jpeg footer + # BLC + 0x4001, 0x02, + 0x4004, 0x02, + # AWB + 0x5180, 0xFF, + 0x5181, 0xF2, + 0x5182, 0x00, + 0x5183, 0x14, + 0x5184, 0x25, + 0x5185, 0x24, + 0x5186, 0x09, + 0x5187, 0x09, + 0x5188, 0x09, + 0x5189, 0x75, + 0x518A, 0x54, + 0x518B, 0xE0, + 0x518C, 0xB2, + 0x518D, 0x42, + 0x518E, 0x3D, + 0x518F, 0x56, + 0x5190, 0x46, + 0x5191, 0xF8, + 0x5192, 0x04, + 0x5193, 0x70, + 0x5194, 0xF0, + 0x5195, 0xF0, + 0x5196, 0x03, + 0x5197, 0x01, + 0x5198, 0x04, + 0x5199, 0x12, + 0x519A, 0x04, + 0x519B, 0x00, + 0x519C, 0x06, + 0x519D, 0x82, + 0x519E, 0x38, + # color matrix (Saturation) + 0x5381, 0x1E, + 0x5382, 0x5B, + 0x5383, 0x08, + 0x5384, 0x0A, + 0x5385, 0x7E, + 0x5386, 0x88, + 0x5387, 0x7C, + 0x5388, 0x6C, + 0x5389, 0x10, + 0x538A, 0x01, + 0x538B, 0x98, + # CIP control (Sharpness) + 0x5300, 0x10, # sharpness + 0x5301, 0x10, # sharpness + 0x5302, 0x18, # sharpness + 0x5303, 0x19, # sharpness + 0x5304, 0x10, + 0x5305, 0x10, + 0x5306, 0x08, # denoise + 0x5307, 0x16, + 0x5308, 0x40, + 0x5309, 0x10, # sharpness + 0x530A, 0x10, # sharpness + 0x530B, 0x04, # sharpness + 0x530C, 0x06, # sharpness + # GAMMA + 0x5480, 0x01, + 0x5481, 0x00, + 0x5482, 0x1E, + 0x5483, 0x3B, + 0x5484, 0x58, + 0x5485, 0x66, + 0x5486, 0x71, + 0x5487, 0x7D, + 0x5488, 0x83, + 0x5489, 0x8F, + 0x548A, 0x98, + 0x548B, 0xA6, + 0x548C, 0xB8, + 0x548D, 0xCA, + 0x548E, 0xD7, + 0x548F, 0xE3, + 0x5490, 0x1D, + # Special Digital Effects (SDE) (UV adjust) + 0x5580, 0x06, # enable brightness and contrast + 0x5583, 0x40, # special_effect + 0x5584, 0x10, # special_effect + 0x5586, 0x20, # contrast + 0x5587, 0x00, # brightness + 0x5588, 0x00, # brightness + 0x5589, 0x10, + 0x558A, 0x00, + 0x558B, 0xF8, + 0x501D, 0x40, # enable manual offset of contrast +] +_sensor_format_jpeg = [ + _FORMAT_CTRL, 0x00, # YUV422 + _FORMAT_CTRL00, 0x30, # YUYV + _SYSTEM_RESET02, 0x00, # enable everything + _CLOCK_ENABLE02, 0xFF, # enable all clocks + 0x471C, 0x50, # 0xd0 to 0x50 !!! +] + +_sensor_format_raw = [ + _FORMAT_CTRL, 0x03, # RAW (DPC) + _FORMAT_CTRL00, 0x00, # RAW +] + +_sensor_format_grayscale = [ + _FORMAT_CTRL, 0x00, # YUV422 + _FORMAT_CTRL00, 0x10, # Y8 +] + +_sensor_format_yuv422 = [ + _FORMAT_CTRL, 0x00, # YUV422 + _FORMAT_CTRL00, 0x30, # YUYV +] + +_sensor_format_rgb565 = [ + _FORMAT_CTRL, 0x01, # RGB + _FORMAT_CTRL00, 0x61, # RGB565 (BGR) + _SYSTEM_RESET02, 0x1C, # reset jfifo, sfifo, jpg, fmux, avg + _CLOCK_ENABLE02, 0xC3, # reset to how it was before (no jpg clock) + +] + +_ov5640_color_settings = { + OV5640_COLOR_RGB: _sensor_format_rgb565, + OV5640_COLOR_YUV: _sensor_format_yuv422, + OV5640_COLOR_GRAYSCALE: _sensor_format_grayscale, + OV5640_COLOR_JPEG: _sensor_format_jpeg, +} + +_contrast_settings = [ + [0x20, 0x00], # 0 + [0x24, 0x10], # +1 + [0x28, 0x18], # +2 + [0x2c, 0x1c], # +3 + [0x14, 0x14], # -3 + [0x18, 0x18], # -2 + [0x1c, 0x1c], # -1 +] + +_sensor_saturation_levels = [ + [0x1D, 0x60, 0x03, 0x0C, 0x78, 0x84, 0x7D, 0x6B, 0x12, 0x01, 0x98], # 0 + [0x1D, 0x60, 0x03, 0x0D, 0x84, 0x91, 0x8A, 0x76, 0x14, 0x01, 0x98], # +1 + [0x1D, 0x60, 0x03, 0x0E, 0x90, 0x9E, 0x96, 0x80, 0x16, 0x01, 0x98], # +2 + [0x1D, 0x60, 0x03, 0x10, 0x9C, 0xAC, 0xA2, 0x8B, 0x17, 0x01, 0x98], # +3 + [0x1D, 0x60, 0x03, 0x11, 0xA8, 0xB9, 0xAF, 0x96, 0x19, 0x01, 0x98], # +4 + [0x1D, 0x60, 0x03, 0x07, 0x48, 0x4F, 0x4B, 0x40, 0x0B, 0x01, 0x98], # -4 + [0x1D, 0x60, 0x03, 0x08, 0x54, 0x5C, 0x58, 0x4B, 0x0D, 0x01, 0x98], # -3 + [0x1D, 0x60, 0x03, 0x0A, 0x60, 0x6A, 0x64, 0x56, 0x0E, 0x01, 0x98], # -2 + [0x1D, 0x60, 0x03, 0x0B, 0x6C, 0x77, 0x70, 0x60, 0x10, 0x01, 0x98], # -1 +] + +_sensor_ev_levels = [ + [0x38, 0x30, 0x61, 0x38, 0x30, 0x10], # 0 + [0x40, 0x38, 0x71, 0x40, 0x38, 0x10], # +1 + [0x50, 0x48, 0x90, 0x50, 0x48, 0x20], # +2 + [0x60, 0x58, 0xa0, 0x60, 0x58, 0x20], # +3 + [0x10, 0x08, 0x10, 0x08, 0x20, 0x10], # -3 + [0x20, 0x18, 0x41, 0x20, 0x18, 0x10], # -2 + [0x30, 0x28, 0x61, 0x30, 0x28, 0x10], # -1 +] + +OV5640_WHITE_BALANCE_AUTO = 0 +OV5640_WHITE_BALANCE_SUNNY = 1 +OV5640_WHITE_BALANCE_FLUORESCENT = 2 +OV5640_WHITE_BALANCE_CLOUDY = 3 +OV5640_WHITE_BALANCE_INCANDESCENT = 4 + +_light_registers = [0x3406, 0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405] +_light_modes = [ + [0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00], # auto + [0x01, 0x06, 0x1c, 0x04, 0x00, 0x04, 0xf3], # sunny + [0x01, 0x05, 0x48, 0x04, 0x00, 0x07, 0xcf], # office / fluorescent + [0x01, 0x06, 0x48, 0x04, 0x00, 0x04, 0xd3], # cloudy + [0x01, 0x04, 0x10, 0x04, 0x00, 0x08, 0x40], # home / incandescent + +] + +OV5640_SPECIAL_EFFECT_NONE = 0 +OV5640_SPECIAL_EFFECT_NEGATIVE = 1 +OV5640_SPECIAL_EFFECT_GRAYSCALE = 2 +OV5640_SPECIAL_EFFECT_RED_TINT = 3 +OV5640_SPECIAL_EFFECT_GREEN_TINT = 4 +OV5640_SPECIAL_EFFECT_BLUE_TINT = 5 +OV5640_SPECIAL_EFFECT_SEPIA = 6 + +_sensor_special_effects = [ + [0x06, 0x40, 0x10, 0x08], # Normal + [0x46, 0x40, 0x28, 0x08], # Negative + [0x1E, 0x80, 0x80, 0x08], # Grayscale + [0x1E, 0x80, 0xC0, 0x08], # Red Tint + [0x1E, 0x60, 0x60, 0x08], # Green Tint + [0x1E, 0xA0, 0x40, 0x08], # Blue Tint + [0x1E, 0x40, 0xA0, 0x08], # Sepia +] + +_sensor_regs_gamma0 = [ + 0x5480, 0x01, + 0x5481, 0x08, + 0x5482, 0x14, + 0x5483, 0x28, + 0x5484, 0x51, + 0x5485, 0x65, + 0x5486, 0x71, + 0x5487, 0x7D, + 0x5488, 0x87, + 0x5489, 0x91, + 0x548A, 0x9A, + 0x548B, 0xAA, + 0x548C, 0xB8, + 0x548D, 0xCD, + 0x548E, 0xDD, + 0x548F, 0xEA, + 0x5490, 0x1D, +] + +sensor_regs_gamma1 = [ + 0x5480, 0x1, + 0x5481, 0x0, + 0x5482, 0x1E, + 0x5483, 0x3B, + 0x5484, 0x58, + 0x5485, 0x66, + 0x5486, 0x71, + 0x5487, 0x7D, + 0x5488, 0x83, + 0x5489, 0x8F, + 0x548A, 0x98, + 0x548B, 0xA6, + 0x548C, 0xB8, + 0x548D, 0xCA, + 0x548E, 0xD7, + 0x548F, 0xE3, + 0x5490, 0x1D, +] + +sensor_regs_awb0 = [ + 0x5180, 0xFF, + 0x5181, 0xF2, + 0x5182, 0x00, + 0x5183, 0x14, + 0x5184, 0x25, + 0x5185, 0x24, + 0x5186, 0x09, + 0x5187, 0x09, + 0x5188, 0x09, + 0x5189, 0x75, + 0x518A, 0x54, + 0x518B, 0xE0, + 0x518C, 0xB2, + 0x518D, 0x42, + 0x518E, 0x3D, + 0x518F, 0x56, + 0x5190, 0x46, + 0x5191, 0xF8, + 0x5192, 0x04, + 0x5193, 0x70, + 0x5194, 0xF0, + 0x5195, 0xF0, + 0x5196, 0x03, + 0x5197, 0x01, + 0x5198, 0x04, + 0x5199, 0x12, + 0x519A, 0x04, + 0x519B, 0x00, + 0x519C, 0x06, + 0x519D, 0x82, + 0x519E, 0x38, +] +# fmt: on + + +class _RegBits: + def __init__(self, reg: int, shift: int, mask: int) -> None: + self.reg = reg + self.shift = shift + self.mask = mask + + def __get__(self, obj: "OV5640", objtype: Optional[type] = None) -> int: + reg_value = obj._read_register(self.reg) + return (reg_value >> self.shift) & self.mask + + def __set__(self, obj: "OV5640", value: int) -> None: + if value & ~self.mask: + raise ValueError( + f"Value 0x{value:02x} does not fit in mask 0x{self.mask:02x}" + ) + reg_value = obj._read_register(self.reg) + reg_value &= ~(self.mask << self.shift) + reg_value |= value << self.shift + obj._write_register(self.reg, reg_value) + + +class _RegBits16: + def __init__(self, reg: int, shift: int, mask: int) -> None: + self.reg = reg + self.shift = shift + self.mask = mask + + def __get__(self, obj: "OV5640", objtype: Optional[type] = None) -> int: + reg_value = obj._read_register16(self.reg) + return (reg_value >> self.shift) & self.mask + + def __set__(self, obj: "OV5640", value: int) -> None: + if value & ~self.mask: + raise ValueError( + f"Value 0x{value:02x} does not fit in mask 0x{self.mask:02x}" + ) + reg_value = obj._read_register16(self.reg) + reg_value &= ~(self.mask << self.shift) + reg_value |= value << self.shift + obj._write_register16(self.reg, reg_value) + + +class _SCCB16CameraBase: # pylint: disable=too-few-public-methods + _finalize_firmware_load = ( + 0x3022, + 0x00, + 0x3023, + 0x00, + 0x3024, + 0x00, + 0x3025, + 0x00, + 0x3026, + 0x00, + 0x3027, + 0x00, + 0x3028, + 0x00, + 0x3029, + 0x7F, + 0x3000, + 0x00, + ) + + def __init__(self, i2c_bus: I2C, i2c_address: int) -> None: + self._i2c_device = I2CDevice(i2c_bus, i2c_address) + self._bank = None + + def _write_register(self, reg: int, value: int) -> None: + b = bytearray(3) + b[0] = reg >> 8 + b[1] = reg & 0xFF + b[2] = value + with self._i2c_device as i2c: + i2c.write(b) + + def _write_addr_reg(self, reg: int, x_value: int, y_value: int) -> None: + self._write_register16(reg, x_value) + self._write_register16(reg + 2, y_value) + + def _write_register16(self, reg: int, value: int) -> None: + self._write_register(reg, value >> 8) + self._write_register(reg + 1, value & 0xFF) + + def _read_register(self, reg: int) -> int: + b = bytearray(2) + b[0] = reg >> 8 + b[1] = reg & 0xFF + with self._i2c_device as i2c: + i2c.write(b) + i2c.readinto(b, end=1) + return b[0] + + def _read_register16(self, reg: int) -> int: + high = self._read_register(reg) + low = self._read_register(reg + 1) + return (high << 8) | low + + def _write_list(self, reg_list: Sequence[int]) -> None: + for i in range(0, len(reg_list), 2): + register = reg_list[i] + value = reg_list[i + 1] + if register == _REG_DLY: + time.sleep(value / 1000) + else: + self._write_register(register, value) + + def _write_reg_bits(self, reg: int, mask: int, enable: bool) -> None: + val = val = self._read_register(reg) + if enable: + val |= mask + else: + val &= ~mask + self._write_register(reg, val) + + +class OV5640(_SCCB16CameraBase): # pylint: disable=too-many-instance-attributes + """Control & Capture Images from an OV5640 Camera""" + + def __init__( + self, + i2c_bus: I2C, + data_pins: List[Pin], + clock: Pin, + vsync: Pin, + href: Pin, + shutdown: Optional[DigitalInOut] = None, + reset: Optional[DigitalInOut] = None, + mclk: Optional[Pin] = None, + mclk_frequency: int = 20_000_000, + i2c_address: int = 0x3C, + size: int = OV5640_SIZE_QQVGA, + init_autofocus: bool = True, + ): # pylint: disable=too-many-arguments + """ + Args: + i2c_bus (busio.I2C): The I2C bus used to configure the OV5640 + data_pins (List[microcontroller.Pin]): A list of 8 data pins, in order. + clock (microcontroller.Pin): The pixel clock from the OV5640. + vsync (microcontroller.Pin): The vsync signal from the OV5640. + href (microcontroller.Pin): The href signal from the OV5640, \ + sometimes inaccurately called hsync. + shutdown (Optional[digitalio.DigitalInOut]): If not None, the shutdown + signal to the camera, also called the powerdown or enable pin. + reset (Optional[digitalio.DigitalInOut]): If not None, the reset signal + to the camera. + mclk (Optional[microcontroller.Pin]): The pin on which to create a + master clock signal, or None if the master clock signal is + already being generated. + mclk_frequency (int): The frequency of the master clock to generate, \ + ignored if mclk is None, requred if it is specified. + Note that the OV5640 requires a very low jitter clock, + so only specific (microcontroller-dependent) values may + work reliably. On the ESP32-S2, a 20MHz clock can be generated + with sufficiently low jitter. + i2c_address (int): The I2C address of the camera. + size (int): The captured image size + init_autofocus (bool): initialize autofocus + """ + + # Initialize the master clock + if mclk: + self._mclk_pwm = pwmio.PWMOut(mclk, frequency=mclk_frequency) + self._mclk_pwm.duty_cycle = 32768 + else: + self._mclk_pwm = None + + if reset: + self._reset = reset + self._reset.switch_to_output(False) + else: + self._reset = None + + if shutdown: + self._shutdown = shutdown + self._shutdown.switch_to_output(True) + time.sleep(0.005) # t2, 5ms stability + self._shutdown.switch_to_output(False) + else: + self._shutdown = None + + if self._reset: + time.sleep(0.001) # t3, 1ms delay from pwdn + self._reset.switch_to_output(True) + time.sleep(0.02) + + # Now that the master clock is running, we can initialize i2c comms + super().__init__(i2c_bus, i2c_address) + + self._write_list(_sensor_default_regs) + + self._imagecapture = imagecapture.ParallelImageCapture( + data_pins=data_pins, clock=clock, vsync=vsync, href=href + ) + + self._colorspace = OV5640_COLOR_RGB + self._flip_x = False + self._flip_y = False + self._w = None + self._h = None + self._size = None + self._test_pattern = False + self._binning = False + self._scale = False + self._ev = 0 + self._white_balance = 0 + self.size = size + + if init_autofocus: + self.autofocus_init() + + chip_id = _RegBits16(_CHIP_ID_HIGH, 0, 0xFFFF) + + def autofocus_init_from_file(self, filename): + """Initialize the autofocus engine from a .bin file""" + with open(filename, mode="rb") as file: + firmware = file.read() + self.autofocus_init_from_bitstream(firmware) + + def autofocus_init_from_bitstream(self, firmware: bytes): + """Initialize the autofocus engine from a bytestring""" + self._write_register(0x3000, 0x20) # reset autofocus coprocessor + time.sleep(0.01) + + arr = bytearray(256) + with self._i2c_device as i2c: + for offset in range(0, len(firmware), 254): + num_firmware_bytes = min(254, len(firmware) - offset) + reg = offset + 0x8000 + arr[0] = reg >> 8 + arr[1] = reg & 0xFF + arr[2 : 2 + num_firmware_bytes] = firmware[ + offset : offset + num_firmware_bytes + ] + i2c.write(arr, end=2 + num_firmware_bytes) + + self._write_list(self._finalize_firmware_load) + for _ in range(100): + if self.autofocus_status == _OV5640_STAT_IDLE: + break + time.sleep(0.01) + else: + raise RuntimeError("Timed out after trying to load autofocus firmware") + + def autofocus_init(self): + """Initialize the autofocus engine from ov5640_autofocus.bin""" + if "/" in __file__: + binfile = ( + __file__.rsplit("/", 1)[0].rsplit(".", 1)[0] + "/ov5640_autofocus.bin" + ) + else: + binfile = "ov5640_autofocus.bin" + print(binfile) + return self.autofocus_init_from_file(binfile) + + @property + def autofocus_status(self): + """Read the camera autofocus status register""" + return self._read_register(_OV5640_CMD_FW_STATUS) + + def _send_autofocus_command(self, command, msg): # pylint: disable=unused-argument + self._write_register(_OV5640_CMD_ACK, 0x01) # clear command ack + self._write_register(_OV5640_CMD_MAIN, command) # send command + for _ in range(1000): + if self._read_register(_OV5640_CMD_ACK) == 0x0: # command is finished + return True + time.sleep(0.01) + return False + + def autofocus(self) -> list[int]: + """Perform an autofocus operation. + + If all elements of the list are 0, the autofocus operation failed. Otherwise, + if at least one element is nonzero, the operation succeeded. + + In principle the elements correspond to 5 autofocus regions, if configured.""" + if not self._send_autofocus_command(_OV5640_CMD_RELEASE_FOCUS, "release focus"): + return [False] * 5 + if not self._send_autofocus_command(_OV5640_CMD_TRIGGER_AUTOFOCUS, "autofocus"): + return [False] * 5 + zone_focus = [self._read_register(_OV5640_CMD_PARA0 + i) for i in range(5)] + print(f"zones focused: {zone_focus}") + return zone_focus + + @property + def autofocus_vcm_step(self): + """Get the voice coil motor step location""" + if not self._send_autofocus_command( + _OV5640_CMD_AF_GET_VCM_STEP, "get vcm step" + ): + return None + return self._read_register(_OV5640_CMD_PARA4) + + @autofocus_vcm_step.setter + def autofocus_vcm_step(self, step): + """Get the voice coil motor step location, from 0 to 255""" + if not 0 <= step <= 255: + raise RuntimeError("VCM step must be 0 to 255") + self._write_register(_OV5640_CMD_PARA3, 0x00) + self._write_register(_OV5640_CMD_PARA4, step) + self._send_autofocus_command(_OV5640_CMD_AF_SET_VCM_STEP, "set vcm step") + + def capture(self, buf: Union[bytearray, memoryview]) -> None: + """Capture an image into the buffer. + + Args: + buf (Union[bytearray, memoryview]): A WritableBuffer to contain the \ + captured image. Note that this can be a ulab array or a displayio Bitmap. + """ + self._imagecapture.capture(buf) + if self.colorspace == OV5640_COLOR_JPEG: + eoi = buf.find(b"\xff\xd9") + if eoi != -1: + # terminate the JPEG data just after the EOI marker + return memoryview(buf)[: eoi + 2] + return None + + @property + def capture_buffer_size(self) -> int: + """Return the size of capture buffer to use with current resolution & colorspace settings""" + if self.colorspace == OV5640_COLOR_JPEG: + return self.width * self.height // self.quality + if self.colorspace == OV5640_COLOR_GRAYSCALE: + return self.width * self.height + return self.width * self.height * 2 + + @property + def mclk_frequency(self) -> Optional[int]: + """Get the actual frequency the generated mclk, or None""" + return self._mclk_pwm.frequency if self._mclk_pwm else None + + @property + def width(self) -> int: + """Get the image width in pixels.""" + return self._w + + @property + def height(self) -> int: + """Get the image height in pixels.""" + return self._h + + @property + def colorspace(self) -> int: + """Get or set the colorspace, one of the ``OV5640_COLOR_`` constants.""" + return self._colorspace + + @colorspace.setter + def colorspace(self, colorspace: int) -> None: + self._colorspace = colorspace + self._set_size_and_colorspace() + + def _set_image_options(self) -> None: # pylint: disable=too-many-branches + reg20 = reg21 = reg4514 = reg4514_test = 0 + if self.colorspace == OV5640_COLOR_JPEG: + reg21 |= 0x20 + + if self._binning: + reg20 |= 1 + reg21 |= 1 + reg4514_test |= 4 + else: + reg20 |= 0x40 + + if self._flip_y: + reg20 |= 0x06 + reg4514_test |= 1 + + if self._flip_x: + reg21 |= 0x06 + reg4514_test |= 2 + + if reg4514_test == 0: + reg4514 = 0x88 + elif reg4514_test == 1: + reg4514 = 0x00 + elif reg4514_test == 2: + reg4514 = 0xBB + elif reg4514_test == 3: + reg4514 = 0x00 + elif reg4514_test == 4: + reg4514 = 0xAA + elif reg4514_test == 5: + reg4514 = 0xBB + elif reg4514_test == 6: + reg4514 = 0xBB + elif reg4514_test == 7: + reg4514 = 0xAA + + self._write_register(_TIMING_TC_REG20, reg20) + self._write_register(_TIMING_TC_REG21, reg21) + self._write_register(0x4514, reg4514) + + if self._binning: + self._write_register(0x4520, 0x0B) + self._write_register(_X_INCREMENT, 0x31) + self._write_register(_Y_INCREMENT, 0x31) + else: + self._write_register(0x4520, 0x10) + self._write_register(_X_INCREMENT, 0x11) + self._write_register(_Y_INCREMENT, 0x11) + + def _set_colorspace(self) -> None: + colorspace = self._colorspace + settings = _ov5640_color_settings[colorspace] + + self._write_list(settings) + + def deinit(self) -> None: + """Deinitialize the camera""" + self._imagecapture.deinit() + if self._mclk_pwm: + self._mclk_pwm.deinit() + if self._shutdown: + self._shutdown.deinit() + if self._reset: + self._reset.deinit() + + @property + def size(self) -> int: + """Get or set the captured image size, one of the ``OV5640_SIZE_`` constants.""" + return self._size + + def _set_size_and_colorspace(self) -> None: # pylint: disable=too-many-locals + size = self._size + width, height, ratio = _resolution_info[size] + self._w = width + self._h = height + ( + max_width, + max_height, + start_x, + start_y, + end_x, + end_y, + offset_x, + offset_y, + total_x, + total_y, + ) = _ratio_table[ratio] + + self._binning = (width <= max_width // 2) and (height <= max_height // 2) + self._scale = not ( + (width == max_width and height == max_height) + or (width == max_width // 2 and height == max_height // 2) + ) + + self._write_addr_reg(_X_ADDR_ST_H, start_x, start_y) + self._write_addr_reg(_X_ADDR_END_H, end_x, end_y) + self._write_addr_reg(_X_OUTPUT_SIZE_H, width, height) + + if not self._binning: + self._write_addr_reg(_X_TOTAL_SIZE_H, total_x, total_y) + self._write_addr_reg(_X_OFFSET_H, offset_x, offset_y) + else: + if width > 920: + self._write_addr_reg(_X_TOTAL_SIZE_H, total_x - 200, total_y // 2) + else: + self._write_addr_reg(_X_TOTAL_SIZE_H, 2060, total_y // 2) + self._write_addr_reg(_X_OFFSET_H, offset_x // 2, offset_y // 2) + + self._write_reg_bits(_ISP_CONTROL_01, 0x20, self._scale) + + self._set_image_options() + + if self.colorspace == OV5640_COLOR_JPEG: + sys_mul = 200 + if size < OV5640_SIZE_QVGA: + sys_mul = 160 + if size < OV5640_SIZE_XGA: + sys_mul = 180 + self._set_pll(False, sys_mul, 4, 2, False, 2, True, 4) + else: + self._set_pll(False, 32, 1, 1, False, 1, True, 4) + + self._set_colorspace() + + def _set_pll( # pylint: disable=too-many-arguments + self, + bypass: bool, + multiplier: int, + sys_div: int, + pre_div: int, + root_2x: bool, + pclk_root_div: int, + pclk_manual: bool, + pclk_div: int, + ) -> None: + if ( # pylint: disable=too-many-boolean-expressions + multiplier > 252 + or multiplier < 4 + or sys_div > 15 + or pre_div > 8 + or pclk_div > 31 + or pclk_root_div > 3 + ): + raise ValueError("Invalid argument to internal function") + + self._write_register(0x3039, 0x80 if bypass else 0) + self._write_register(0x3034, 0x1A) + self._write_register(0x3035, 1 | ((sys_div & 0xF) << 4)) + self._write_register(0x3036, multiplier & 0xFF) + self._write_register(0x3037, (pre_div & 0xF) | (0x10 if root_2x else 0)) + self._write_register(0x3108, (pclk_root_div & 3) << 4 | 0x06) + self._write_register(0x3824, pclk_div & 0x1F) + self._write_register(0x460C, 0x22 if pclk_manual else 0x22) + self._write_register(0x3103, 0x13) + + @size.setter + def size(self, size: int) -> None: + self._size = size + self._set_size_and_colorspace() + + @property + def flip_x(self) -> bool: + """Get or set the X-flip flag""" + return self._flip_x + + @flip_x.setter + def flip_x(self, value: bool) -> None: + self._flip_x = bool(value) + self._set_image_options() + + @property + def flip_y(self) -> bool: + """Get or set the Y-flip flag""" + return self._flip_y + + @flip_y.setter + def flip_y(self, value: bool) -> None: + self._flip_y = bool(value) + self._set_image_options() + + @property + def test_pattern(self) -> bool: + """Set to True to enable a test pattern, False to enable normal image capture""" + return self._test_pattern + + @test_pattern.setter + def test_pattern(self, value: bool) -> None: + self._test_pattern = value + self._write_register(_PRE_ISP_TEST_SETTING_1, value << 7) + + @property + def saturation(self) -> int: + """Get or set the saturation value, from -4 to +4.""" + return self._saturation + + @saturation.setter + def saturation(self, value: int) -> None: + if not -4 <= value <= 4: + raise ValueError( + "Invalid saturation {value}, use a value from -4..4 inclusive" + ) + for offset, reg_value in enumerate(_sensor_saturation_levels[value]): + self._write_register(0x5381 + offset, reg_value) + self._saturation = value + + @property + def effect(self) -> int: + """Get or set the special effect, one of the ``OV5640_SPECIAL_EFFECT_`` constants""" + return self._effect + + @effect.setter + def effect(self, value: int) -> None: + for reg_addr, reg_value in zip( + (0x5580, 0x5583, 0x5584, 0x5003), _sensor_special_effects[value] + ): + self._write_register(reg_addr, reg_value) + self._effect = value + + @property + def quality(self) -> int: + """Controls the JPEG quality. Valid range is from 2..55 inclusive""" + return self._read_register(_COMPRESSION_CTRL07) & 0x3F + + @quality.setter + def quality(self, value: int) -> None: + if not 2 <= value < 55: + raise ValueError( + f"Invalid quality value {value}, use a value from 2..55 inclusive" + ) + self._write_register(_COMPRESSION_CTRL07, value & 0x3F) + + def _write_group_3_settings(self, settings): + self._write_register(0x3212, 0x3) # start group 3 + self._write_list(settings) + self._write_register(0x3212, 0x13) # end group 3 + self._write_register(0x3212, 0xA3) # launch group 3 + + @property + def brightness(self) -> int: + """Sensor brightness adjustment, from -4 to 4 inclusive""" + brightness_abs = self._read_register(0x5587) >> 4 + brightness_neg = self._read_register(0x5588) & 8 + if brightness_neg: + return -brightness_abs + return brightness_abs + + @brightness.setter + def brightness(self, value: int) -> None: + if not -4 <= value <= 4: + raise ValueError( + "Invalid brightness value {value}, use a value from -4..4 inclusive" + ) + self._write_group_3_settings( + [0x5587, abs(value) << 4, 0x5588, 0x9 if value < 0 else 0x1] + ) + + @property + def contrast(self) -> int: + """Sensor contrast adjustment, from -4 to 4 inclusive""" + contrast_abs = self._read_register(0x5587) >> 4 + contrast_neg = self._read_register(0x5588) & 8 + if contrast_neg: + return -contrast_abs + return contrast_abs + + @contrast.setter + def contrast(self, value: int) -> None: + if not -3 <= value <= 3: + raise ValueError( + "Invalid contrast value {value}, use a value from -3..3 inclusive" + ) + setting = _contrast_settings[value] + self._write_group_3_settings([0x5586, setting[0], 0x5585, setting[1]]) + + @property + def exposure_value(self) -> int: + """Sensor exposure (EV) adjustment, from -4 to 4 inclusive""" + return self._ev + + @exposure_value.setter + def exposure_value(self, value: int) -> None: + if not -3 <= value <= 3: + raise ValueError( + "Invalid exposure value (EV) {value}, use a value from -4..4 inclusive" + ) + for offset, reg_value in enumerate(_sensor_ev_levels[value]): + self._write_register(0x5381 + offset, reg_value) + + @property + def white_balance(self) -> int: + """The white balance setting, one of the ``OV5640_WHITE_BALANCE_*`` constants""" + return self._white_balance + + @white_balance.setter + def white_balance(self, value: int) -> None: + if not OV5640_WHITE_BALANCE_AUTO <= value <= OV5640_WHITE_BALANCE_INCANDESCENT: + raise ValueError( + "Invalid exposure value (EV) {value}, " + "use one of the OV5640_WHITE_BALANCE_* constants" + ) + self._write_register(0x3212, 0x3) # start group 3 + for reg_addr, reg_value in zip(_light_registers, _light_modes[value]): + self._write_register(reg_addr, reg_value) + self._write_register(0x3212, 0x13) # end group 3 + self._write_register(0x3212, 0xA3) # launch group 3 + + @property + def night_mode(self) -> bool: + """Enable or disable the night mode setting of the sensor""" + return bool(self._read_register(0x3A00) & 0x04) + + @night_mode.setter + def night_mode(self, value: bool) -> None: + self._write_reg_bits(0x3A00, 0x04, value) diff --git a/FC_Board/lib/adafruit_ov5640/ov5640_autofocus.bin b/FC_Board/lib/adafruit_ov5640/ov5640_autofocus.bin new file mode 100644 index 0000000000000000000000000000000000000000..06e4b5adbc39a1058b79faf9cdc0e57445228280 GIT binary patch literal 4077 zcmZu!d2kd}8t>^j(sN{H?0^P$18jpXC`X4mGJzb85MZIojTlk!*tmt3s7qxvL%pa= zh6!Vd5D>C!VhRsTtx~R9O2UlpVnL9aS%vi<$KCD@M~>F29uhJ$z2EB&pj$gN z@AY^5-uJ%uzVCbensj|b+Dc;vh9OAT`{JCs=atzBpE`a=YW}?wLYhju2E0G=&ZrPo z#SQW9YEz>*L^e*L&-(Icmx(hq8i8fer+s-+VVaWA$7%Gbgg!~5f$biFN~4dq8-nXe z^dVTPSJ6p$1m_q45|quo$_Cn|#!49Uk(BHpIPz9ToO9efPf+H>4Cq5CM@cHWx6eZL z<-JiNc$6}pT_6W%yAwV}+yLWup-}+X_q~T{?<8G z*vaK%x4^j%HGA4H(8f^=YpXRaIDJyn>T_eA3Q81Dg9p5GpHN6UFL7j|1WZwPQfYC{ z7DL^7DUJqd=Xac$c7Ds1(k>GABnNv!;w?fSOY@CrAdR0{G?>O}3mTNvkO>XI>Q40K z1?P}>lSpzs*duF=BzYG)A*q_x6i0#8YLG3^4Xa?-Fl`ttV7oKGY@rqkuu3)(XrM1I zNl9Bm9U;y%EgJ00ix*1{sA+8+eDugKMktj z$tA}-Tx0qkHxeN;$W1DGtuGH2Zm>xZMbjVzmcgqDX{*6hSYOvR2{oq?^eyQ|11_}R zPR7wrX@8+KYk#$kc7$OFPT%@%OD9pY6yIM>0Xcz3>F6YC($SoT4&Ow#NXh-&G`c12 z8sFlsI}&f$IgFY*)mg)k%e!zc#~Yf5QA;P4PGVJKC)RLg>o66v#2Xri(M}zlL$7EK z13HQJ+}x!-!>Aedwhga#3t6|7ckYdm`@4&31T70cd5r@MmRGOl&SHh>X;`t;>nC1 z9y!37;gNHFEngDOW|`3mMIPHVH}T8dFB_+T-w0=?grRG~ znaQJJ<|E?d$Hc>*5Q|6l$h*EeJCqUS)03!BHwM1LDzA8^+~-cHkQ*;hFZ_T- z<8cp;g6<-SglMMT5Y5(;(OkVTsxs0PB~4P&HA*@?!HlG-(KI!drpD7u?;T+!EXuwn zrHMJ$bS^w0MC)i6T9@C`6P*A;3BTGTya0xtt{v~`=?Q(u2TW)qZO`%nB9-e&Wk=OG zZ9l>X+_b%&4_Hu*rXzo*qe0h`I$oTIHa2Xe9R++~M(X$+F6SOIbHfz<@Uj^c0 zMqRW?7p=qo9T!(kO(P%G#cOsDSn=U?^h26BjvkRZC|pFZ%@%s_PIxo= zbLc4;h91@#jCckRwEZl|EzMfpQqx=)cKjUH>vG!-z6(7e@wEmSJt_6bXe;g>fbLHQ z^aO3cG~Ynm`_Pk^A~CfUQ$|ev0aGTR@;@{GGvpuA?y1)ZAEQ1D{m{> zYb$EE6-8}D9k!waw%fXGw~r8nEkL{K8E6S31OJ$j(XS??ttbWuQ_M)`=LcukC43+t zZKepfK2nBnIlxBNbCC^YKnMz>%!_cvkxDjFu{2V?gm$e7%?UC@aGs%FRTe9Yl)II? zKp0ufMHW>??yiX3RW2xHp*q@iYv|cXc~zuhjkq>mpo&#+=#p(!;yTQ%(HLEm6{{9! z1y4p?O}lu`EUxZWbW~PcqbpYFidEgp8rpI`G$X>Ti&U-!%V0lbAuZ?W{2j2JV!7NS zRzRzS#z8BSd3K&4GlQwYror&wIlj`7QkIF!*#FeC{-^lbSt-Q^n{MZ8i&DxApl0&5 zze*_%pz`_Jg(<}el#8z|PAP7nZsBW7Qwj}K0c)_Z9%C|Oh__o=@&;{VJr34rol9Eg z8g#9lXAP;5I3L^UU5ZLiLydm zUgcMoi=|aQrBn=5d6j^k3M+~u%h^cj(nw$l=~|QONd;xHSA9>03tD#?DmKliqU^10 z3oA_{GJ`hmAhf$KX*vlthimdtDUJ)e0Cd`|8N|%+=My#Zsi&`6hd-}zt-H!dnbDwZ z>Sbul%Wdmg*S4%_u41&~Z!A4ZrI`XXE_K6O{78-gV^_hL^(u3$^xc5A)&+P!T1uM( z@07glf6I5&%S2U}1npl0`lp7rA_naq zzcXDZ_|r^qw=sCs5d6p#^r}IhGw7cgEXk*x2Wi3zRX`J#1d;G2`mbw$6D%=;mZhvQ zFHN0}Q3)?=x5oT5`CQDN@UvD-6V8|wXr~bpp7kYpcA+?1KJGSa2_5r=P72Bf-uWUd zt?hqe$vdCD${$?%Z;%Qv}g zz(K4C(Vr7Q zo=00YAvf(hD5yEnmlZO|2@=|M82GH{)dp@t6abj(k<$WS+{(5rZC=7eCnreDRsw#s z;Wnyx=KVFR=i*<1!EM;Y-OeuDL9nK|-?3(HpxQ`etAB0C)e^T5m1dTxv@k@a;Vz<* zBnZDHkEpb}WNe5va09G?7+@FH5z7fe#m#T@lPgz0G`G-gGnojZx`iM@7)ubO$zmal z_Yp5vmo1p>c2_T6aI4!*5^jgv% z7V8)1RYB4}R_ml)r&G!z+I1?WEDkw<6ImHJu~>d%S$jY8KUY}HUcb;XQ_0ao^`VL9 z4nVxNiri9#6Duo}O0l9`sSwM{l=A6=m#@rEDIU=y-#EyArypcWxV4EvjY9fDHn?+$ PbR1{{0aX4dX?gR%8vtA( literal 0 HcmV?d00001 diff --git a/FC_Board/lib/adafruit_ov5640/ov5640_autofocus.bin.license b/FC_Board/lib/adafruit_ov5640/ov5640_autofocus.bin.license new file mode 100644 index 00000000..bbb9ced1 --- /dev/null +++ b/FC_Board/lib/adafruit_ov5640/ov5640_autofocus.bin.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Unknown + +SPDX-License-Identifier: GPL-2.0-only diff --git a/FC_Board/lib/adafruit_pca9685.py b/FC_Board/lib/adafruit_pca9685.py new file mode 100644 index 00000000..789c6272 --- /dev/null +++ b/FC_Board/lib/adafruit_pca9685.py @@ -0,0 +1,189 @@ +# SPDX-FileCopyrightText: 2016 Radomir Dopieralski for Adafruit Industries +# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_pca9685` +==================================================== + +Driver for the PCA9685 PWM control IC. Its commonly used to control servos, leds and motors. + +.. seealso:: The `Adafruit CircuitPython Motor library + `_ can be used to control the PWM + outputs for specific uses instead of generic duty_cycle adjustments. + +* Author(s): Scott Shawcroft + +Implementation Notes +-------------------- + +**Hardware:** + +* Adafruit `16-Channel 12-bit PWM/Servo Driver - I2C interface - PCA9685 + `_ (Product ID: 815) + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: + https://github.com/adafruit/circuitpython/releases +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +* Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PCA9685.git" + +import time + +from adafruit_register.i2c_struct import UnaryStruct +from adafruit_register.i2c_struct_array import StructArray +from adafruit_bus_device import i2c_device + +try: + from typing import Optional, Type + from types import TracebackType + from busio import I2C +except ImportError: + pass + + +class PWMChannel: + """A single PCA9685 channel that matches the :py:class:`~pwmio.PWMOut` API. + + :param PCA9685 pca: The PCA9685 object + :param int index: The index of the channel + """ + + def __init__(self, pca: "PCA9685", index: int): + self._pca = pca + self._index = index + + @property + def frequency(self) -> float: + """The overall PWM frequency in Hertz (read-only). + A PWMChannel's frequency cannot be set individually. + All channels share a common frequency, set by PCA9685.frequency.""" + return self._pca.frequency + + @frequency.setter + def frequency(self, _): + raise NotImplementedError("frequency cannot be set on individual channels") + + @property + def duty_cycle(self) -> int: + """16 bit value that dictates how much of one cycle is high (1) versus low (0). 0xffff will + always be high, 0 will always be low and 0x7fff will be half high and then half low. + """ + pwm = self._pca.pwm_regs[self._index] + if pwm[0] == 0x1000: + return 0xFFFF + return pwm[1] << 4 + + @duty_cycle.setter + def duty_cycle(self, value: int) -> None: + if not 0 <= value <= 0xFFFF: + raise ValueError(f"Out of range: value {value} not 0 <= value <= 65,535") + + if value == 0xFFFF: + self._pca.pwm_regs[self._index] = (0x1000, 0) + else: + # Shift our value by four because the PCA9685 is only 12 bits but our value is 16 + value = (value + 1) >> 4 + self._pca.pwm_regs[self._index] = (0, value) + + +class PCAChannels: # pylint: disable=too-few-public-methods + """Lazily creates and caches channel objects as needed. Treat it like a sequence. + + :param PCA9685 pca: The PCA9685 object + """ + + def __init__(self, pca: "PCA9685") -> None: + self._pca = pca + self._channels = [None] * len(self) + + def __len__(self) -> int: + return 16 + + def __getitem__(self, index: int) -> PWMChannel: + if not self._channels[index]: + self._channels[index] = PWMChannel(self._pca, index) + return self._channels[index] + + +class PCA9685: + """ + Initialise the PCA9685 chip at ``address`` on ``i2c_bus``. + + The internal reference clock is 25mhz but may vary slightly with environmental conditions and + manufacturing variances. Providing a more precise ``reference_clock_speed`` can improve the + accuracy of the frequency and duty_cycle computations. See the ``calibration.py`` example for + how to derive this value by measuring the resulting pulse widths. + + :param ~busio.I2C i2c_bus: The I2C bus which the PCA9685 is connected to. + :param int address: The I2C address of the PCA9685. + :param int reference_clock_speed: The frequency of the internal reference clock in Hertz. + """ + + # Registers: + mode1_reg = UnaryStruct(0x00, " None: + self.i2c_device = i2c_device.I2CDevice(i2c_bus, address) + self.channels = PCAChannels(self) + """Sequence of 16 `PWMChannel` objects. One for each channel.""" + self.reference_clock_speed = reference_clock_speed + """The reference clock speed in Hz.""" + self.reset() + + def reset(self) -> None: + """Reset the chip.""" + self.mode1_reg = 0x00 # Mode1 + + @property + def frequency(self) -> float: + """The overall PWM frequency in Hertz.""" + prescale_result = self.prescale_reg + if prescale_result < 3: + raise ValueError( + "The device pre_scale register (0xFE) was not read or returned a value < 3" + ) + return self.reference_clock_speed / 4096 / prescale_result + + @frequency.setter + def frequency(self, freq: float) -> None: + prescale = int(self.reference_clock_speed / 4096.0 / freq + 0.5) + if prescale < 3: + raise ValueError("PCA9685 cannot output at the given frequency") + old_mode = self.mode1_reg # Mode 1 + self.mode1_reg = (old_mode & 0x7F) | 0x10 # Mode 1, sleep + self.prescale_reg = prescale # Prescale + self.mode1_reg = old_mode # Mode 1 + time.sleep(0.005) + # Mode 1, autoincrement on, fix to stop pca9685 from accepting commands at all addresses + self.mode1_reg = old_mode | 0xA0 + + def __enter__(self) -> "PCA9685": + return self + + def __exit__( + self, + exception_type: Optional[Type[type]], + exception_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + self.deinit() + + def deinit(self) -> None: + """Stop using the pca9685.""" + self.reset() diff --git a/FC_Board/lib/adafruit_rfm/rfm9x.py b/FC_Board/lib/adafruit_rfm/rfm9x.py new file mode 100644 index 00000000..7ab8d623 --- /dev/null +++ b/FC_Board/lib/adafruit_rfm/rfm9x.py @@ -0,0 +1,535 @@ +# SPDX-FileCopyrightText: 2024 Jerry Needell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_rfm.rfm9x` +==================================================== + +CircuitPython module for the RFM95/6/7/8 LoRa 433/915mhz radio modules. + +* Author(s): Jerry Needell +""" + +import time + +from micropython import const + +from adafruit_rfm.rfm_common import RFMSPI + +try: + import busio + import digitalio + from circuitpython_typing import ReadableBuffer + + try: + from typing import Literal + except ImportError: + from typing_extensions import Literal + +except ImportError: + pass + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RFM.git" + +# pylint: disable=duplicate-code + +# Internal constants: +# Register names (FSK Mode even though we use LoRa instead, from table 85) +_RF95_REG_00_FIFO = const(0x00) +_RF95_REG_01_OP_MODE = const(0x01) +_RF95_REG_06_FRF_MSB = const(0x06) +_RF95_REG_07_FRF_MID = const(0x07) +_RF95_REG_08_FRF_LSB = const(0x08) +_RF95_REG_09_PA_CONFIG = const(0x09) +_RF95_REG_0A_PA_RAMP = const(0x0A) +_RF95_REG_0B_OCP = const(0x0B) +_RF95_REG_0C_LNA = const(0x0C) +_RF95_REG_0D_FIFO_ADDR_PTR = const(0x0D) +_RF95_REG_0E_FIFO_TX_BASE_ADDR = const(0x0E) +_RF95_REG_0F_FIFO_RX_BASE_ADDR = const(0x0F) +_RF95_REG_10_FIFO_RX_CURRENT_ADDR = const(0x10) +_RF95_REG_11_IRQ_FLAGS_MASK = const(0x11) +_RF95_REG_12_IRQ_FLAGS = const(0x12) +_RF95_REG_13_RX_NB_BYTES = const(0x13) +_RF95_REG_14_RX_HEADER_CNT_VALUE_MSB = const(0x14) +_RF95_REG_15_RX_HEADER_CNT_VALUE_LSB = const(0x15) +_RF95_REG_16_RX_PACKET_CNT_VALUE_MSB = const(0x16) +_RF95_REG_17_RX_PACKET_CNT_VALUE_LSB = const(0x17) +_RF95_REG_18_MODEM_STAT = const(0x18) +_RF95_REG_19_PKT_SNR_VALUE = const(0x19) +_RF95_REG_1A_PKT_RSSI_VALUE = const(0x1A) +_RF95_REG_1B_RSSI_VALUE = const(0x1B) +_RF95_REG_1C_HOP_CHANNEL = const(0x1C) +_RF95_REG_1D_MODEM_CONFIG1 = const(0x1D) +_RF95_REG_1E_MODEM_CONFIG2 = const(0x1E) +_RF95_REG_1F_SYMB_TIMEOUT_LSB = const(0x1F) +_RF95_REG_20_PREAMBLE_MSB = const(0x20) +_RF95_REG_21_PREAMBLE_LSB = const(0x21) +_RF95_REG_22_PAYLOAD_LENGTH = const(0x22) +_RF95_REG_23_MAX_PAYLOAD_LENGTH = const(0x23) +_RF95_REG_24_HOP_PERIOD = const(0x24) +_RF95_REG_25_FIFO_RX_BYTE_ADDR = const(0x25) +_RF95_REG_26_MODEM_CONFIG3 = const(0x26) + +_RF95_REG_40_DIO_MAPPING1 = const(0x40) +_RF95_REG_41_DIO_MAPPING2 = const(0x41) +_RF95_REG_42_VERSION = const(0x42) + +_RF95_REG_4B_TCXO = const(0x4B) +_RF95_REG_4D_PA_DAC = const(0x4D) +_RF95_REG_5B_FORMER_TEMP = const(0x5B) +_RF95_REG_61_AGC_REF = const(0x61) +_RF95_REG_62_AGC_THRESH1 = const(0x62) +_RF95_REG_63_AGC_THRESH2 = const(0x63) +_RF95_REG_64_AGC_THRESH3 = const(0x64) + +_RF95_DETECTION_OPTIMIZE = const(0x31) +_RF95_DETECTION_THRESHOLD = const(0x37) + +_RF95_PA_DAC_DISABLE = const(0x04) +_RF95_PA_DAC_ENABLE = const(0x07) + +# The crystal oscillator frequency of the module +_RF95_FXOSC = 32000000.0 + +# The Frequency Synthesizer step = RH_RF95_FXOSC / 2^^19 +_RF95_FSTEP = _RF95_FXOSC / 524288 + +# RadioHead specific compatibility constants. +_RH_BROADCAST_ADDRESS = const(0xFF) + +# The acknowledgement bit in the FLAGS +# The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved +# for application layer use. +_RH_FLAGS_ACK = const(0x80) +_RH_FLAGS_RETRY = const(0x40) + +# User facing constants: +SLEEP_MODE = 0b000 +STANDBY_MODE = 0b001 +FS_TX_MODE = 0b010 +TX_MODE = 0b011 +FS_RX_MODE = 0b100 +RX_MODE = 0b101 + + +# pylint: disable=too-many-instance-attributes +class RFM9x(RFMSPI): + """Interface to a RFM95/6/7/8 LoRa radio module. Allows sending and + receiving bytes of data in long range LoRa mode at a support board frequency + (433/915mhz). + + You must specify the following parameters: + - spi: The SPI bus connected to the radio. + - cs: The CS pin DigitalInOut connected to the radio. + - reset: The reset/RST pin DigialInOut connected to the radio. + - frequency: The frequency (in mhz) of the radio module (433/915mhz typically). + + You can optionally specify: + - preamble_length: The length in bytes of the packet preamble (default 8). + - high_power: Boolean to indicate a high power board (RFM95, etc.). Default + is True for high power. + - baudrate: Baud rate of the SPI connection, default is 10mhz but you might + choose to lower to 1mhz if using long wires or a breadboard. + - agc: Boolean to Enable/Disable Automatic Gain Control - Default=False (AGC off) + - crc: Boolean to Enable/Disable Cyclic Redundancy Check - Default=True (CRC Enabled) + Remember this library makes a best effort at receiving packets with pure + Python code. Trying to receive packets too quickly will result in lost data + so limit yourself to simple scenarios of sending and receiving single + packets at a time. + + Also note this library tries to be compatible with raw RadioHead Arduino + library communication. This means the library sets up the radio modulation + to match RadioHead's defaults. + Advanced RadioHead features like address/node specific packets + or "reliable datagram" delivery are supported however due to the + limitations noted, "reliable datagram" is still subject to missed packets. + """ + + operation_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, bits=3) + + low_frequency_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=3, bits=1) + + modulation_type = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=5, bits=2) + + # Long range/LoRa mode can only be set in sleep mode! + long_range_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=7, bits=1) + + output_power = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, bits=4) + + max_power = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, offset=4, bits=3) + + pa_select = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, offset=7, bits=1) + + pa_dac = RFMSPI.RegisterBits(_RF95_REG_4D_PA_DAC, bits=3) + + dio0_mapping = RFMSPI.RegisterBits(_RF95_REG_40_DIO_MAPPING1, offset=6, bits=2) + + auto_agc = RFMSPI.RegisterBits(_RF95_REG_26_MODEM_CONFIG3, offset=2, bits=1) + + low_datarate_optimize = RFMSPI.RegisterBits( + _RF95_REG_26_MODEM_CONFIG3, offset=3, bits=1 + ) + + lna_boost_hf = RFMSPI.RegisterBits(_RF95_REG_0C_LNA, offset=0, bits=2) + + auto_ifon = RFMSPI.RegisterBits(_RF95_DETECTION_OPTIMIZE, offset=7, bits=1) + + detection_optimize = RFMSPI.RegisterBits(_RF95_DETECTION_OPTIMIZE, offset=0, bits=3) + + bw_bins = (7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000) + + def __init__( # noqa: PLR0913 + self, + spi: busio.SPI, + cs: digitalio.DigitalInOut, # pylint: disable=invalid-name + rst: digitalio.DigitalInOut, + frequency: int, + *, + preamble_length: int = 8, + high_power: bool = True, + baudrate: int = 5000000, + agc: bool = False, + crc: bool = True, + ) -> None: + super().__init__(spi, cs, baudrate=baudrate) + self.module = "RFM9X" + self.max_packet_length = 252 + self.high_power = high_power + # Device support SPI mode 0 (polarity & phase = 0) up to a max of 10mhz. + # Set Default Baudrate to 5MHz to avoid problems + # self._device = spidev.SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=0) + # Setup reset as a digital output - initially High + # This line is pulled low as an output quickly to trigger a reset. + self._rst = rst + # initialize Reset High + self._rst.switch_to_output(value=True) + self.reset() + # No device type check! Catch an error from the very first request and + # throw a nicer message to indicate possible wiring problems. + version = self.read_u8(address=_RF95_REG_42_VERSION) + if version != 18: + raise RuntimeError( + "Failed to find rfm9x with expected version -- check wiring. Version found:", + hex(version), + ) + + # Set sleep mode, wait 10s and confirm in sleep mode (basic device check). + # Also set long range mode (LoRa mode) as it can only be done in sleep. + self.sleep() + time.sleep(0.01) + self.long_range_mode = True + if self.operation_mode != SLEEP_MODE or not self.long_range_mode: + raise RuntimeError("Failed to configure radio for LoRa mode, check wiring!") + # clear default setting for access to LF registers if frequency > 525MHz + if frequency > 525: + self.low_frequency_mode = 0 + # Setup entire 256 byte FIFO + self.write_u8(_RF95_REG_0E_FIFO_TX_BASE_ADDR, 0x00) + self.write_u8(_RF95_REG_0F_FIFO_RX_BASE_ADDR, 0x00) + # Set mode idle + self.idle() + # Set frequency + self.frequency_mhz = frequency + # Set preamble length (default 8 bytes to match radiohead). + self.preamble_length = preamble_length + # Defaults set modem config to RadioHead compatible Bw125Cr45Sf128 mode. + self.signal_bandwidth = 125000 + self.coding_rate = 5 + self.spreading_factor = 7 + # Default to enable CRC checking on incoming packets. + self.enable_crc = crc + """CRC Enable state""" + # set AGC - Default = False + self.auto_agc = agc + """Automatic Gain Control state""" + # Set transmit power to 13 dBm, a safe value any module supports. + self.tx_power = 13 + + def reset(self) -> None: + """Perform a reset of the chip.""" + # See section 7.2.2 of the datasheet for reset description. + self._rst.value = False # Set Reset Low + time.sleep(0.0001) # 100 us + self._rst.value = True # set Reset High + time.sleep(0.005) # 5 ms + + def idle(self) -> None: + """Enter idle standby mode.""" + self.operation_mode = STANDBY_MODE + + def sleep(self) -> None: + """Enter sleep mode.""" + self.operation_mode = SLEEP_MODE + + def listen(self) -> None: + """Listen for packets to be received by the chip. Use :py:func:`receive` + to listen, wait and retrieve packets as they're available. + """ + self.operation_mode = RX_MODE + self.dio0_mapping = 0b00 # Interrupt on rx done. + + def transmit(self) -> None: + """Transmit a packet which is queued in the FIFO. This is a low level + function for entering transmit mode and more. For generating and + transmitting a packet of data use :py:func:`send` instead. + """ + self.operation_mode = TX_MODE + self.dio0_mapping = 0b01 # Interrupt on tx done. + + @property + def preamble_length(self) -> int: + """The length of the preamble for sent and received packets, an unsigned + 16-bit value. Received packets must match this length or they are + ignored! Set to 8 to match the RadioHead RFM95 library. + """ + msb = self.read_u8(_RF95_REG_20_PREAMBLE_MSB) + lsb = self.read_u8(_RF95_REG_21_PREAMBLE_LSB) + return ((msb << 8) | lsb) & 0xFFFF + + @preamble_length.setter + def preamble_length(self, val: int) -> None: + assert 0 <= val <= 65535 + self.write_u8(_RF95_REG_20_PREAMBLE_MSB, (val >> 8) & 0xFF) + self.write_u8(_RF95_REG_21_PREAMBLE_LSB, val & 0xFF) + + @property + def frequency_mhz(self) -> Literal[433.0, 915.0]: + """The frequency of the radio in Megahertz. Only the allowed values for + your radio must be specified (i.e. 433 vs. 915 mhz)! + """ + msb = self.read_u8(_RF95_REG_06_FRF_MSB) + mid = self.read_u8(_RF95_REG_07_FRF_MID) + lsb = self.read_u8(_RF95_REG_08_FRF_LSB) + frf = ((msb << 16) | (mid << 8) | lsb) & 0xFFFFFF + frequency = (frf * _RF95_FSTEP) / 1000000.0 + return frequency + + @frequency_mhz.setter + def frequency_mhz(self, val: Literal[433.0, 915.0]) -> None: + if val < 240 or val > 960: + raise RuntimeError("frequency_mhz must be between 240 and 960") + # Calculate FRF register 24-bit value. + frf = int((val * 1000000.0) / _RF95_FSTEP) & 0xFFFFFF + # Extract byte values and update registers. + msb = frf >> 16 + mid = (frf >> 8) & 0xFF + lsb = frf & 0xFF + self.write_u8(_RF95_REG_06_FRF_MSB, msb) + self.write_u8(_RF95_REG_07_FRF_MID, mid) + self.write_u8(_RF95_REG_08_FRF_LSB, lsb) + + @property + def tx_power(self) -> int: + """The transmit power in dBm. Can be set to a value from 5 to 23 for + high power devices (RFM95/96/97/98, high_power=True) or -1 to 14 for low + power devices. Only integer power levels are actually set (i.e. 12.5 + will result in a value of 12 dBm). + The actual maximum setting for high_power=True is 20dBm but for values > 20 + the PA_BOOST will be enabled resulting in an additional gain of 3dBm. + The actual setting is reduced by 3dBm. + The reported value will reflect the reduced setting. + """ + if self.high_power: + return self.output_power + 5 + return self.output_power - 1 + + @tx_power.setter + def tx_power(self, val: int) -> None: + val = int(val) + if self.high_power: + if val < 5 or val > 23: + raise RuntimeError("tx_power must be between 5 and 23") + # Enable power amp DAC if power is above 20 dB. + # Lower setting by 3db when PA_BOOST enabled - see Data Sheet Section 6.4 + if val > 20: + self.pa_dac = _RF95_PA_DAC_ENABLE + val -= 3 + else: + self.pa_dac = _RF95_PA_DAC_DISABLE + self.pa_select = True + self.output_power = (val - 5) & 0x0F + else: + assert -1 <= val <= 14 + self.pa_select = False + self.max_power = 0b111 # Allow max power output. + self.output_power = (val + 1) & 0x0F + + @property + def rssi(self) -> float: + """The received strength indicator (in dBm) of the last received message.""" + # Read RSSI register and convert to value using formula in datasheet. + # Remember in LoRa mode the payload register changes function to RSSI! + raw_rssi = self.read_u8(_RF95_REG_1A_PKT_RSSI_VALUE) + if self.low_frequency_mode: + raw_rssi -= 157 + else: + raw_rssi -= 164 + return float(raw_rssi) + + @property + def snr(self) -> float: + """The SNR (in dB) of the last received message.""" + # Read SNR 0x19 register and convert to value using formula in datasheet. + # SNR(dB) = PacketSnr [twos complement] / 4 + snr_byte = self.read_u8(_RF95_REG_19_PKT_SNR_VALUE) + if snr_byte > 127: + snr_byte = (256 - snr_byte) * -1 + return snr_byte / 4 + + @property + def signal_bandwidth(self) -> int: + """The signal bandwidth used by the radio (try setting to a higher + value to increase throughput or to a lower value to increase the + likelihood of successfully received payloads). Valid values are + listed in RFM9x.bw_bins.""" + bw_id = (self.read_u8(_RF95_REG_1D_MODEM_CONFIG1) & 0xF0) >> 4 + if bw_id >= len(self.bw_bins): + current_bandwidth = 500000 + else: + current_bandwidth = self.bw_bins[bw_id] + return current_bandwidth + + @signal_bandwidth.setter + def signal_bandwidth(self, val: int) -> None: + # Set signal bandwidth (set to 125000 to match RadioHead Bw125). + for bw_id, cutoff in enumerate(self.bw_bins): + if val <= cutoff: + break + else: + bw_id = 9 + self.write_u8( + _RF95_REG_1D_MODEM_CONFIG1, + (self.read_u8(_RF95_REG_1D_MODEM_CONFIG1) & 0x0F) | (bw_id << 4), + ) + if val >= 500000: + # see Semtech SX1276 errata note 2.3 + self.auto_ifon = True + # see Semtech SX1276 errata note 2.1 + if self.low_frequency_mode: + self.write_u8(0x36, 0x02) + self.write_u8(0x3A, 0x7F) + else: + self.write_u8(0x36, 0x02) + self.write_u8(0x3A, 0x64) + else: + # see Semtech SX1276 errata note 2.3 + self.auto_ifon = False + self.write_u8(0x36, 0x03) + if val == 7800: + self.write_u8(0x2F, 0x48) + elif val >= 62500: + # see Semtech SX1276 errata note 2.3 + self.write_u8(0x2F, 0x40) + else: + self.write_u8(0x2F, 0x44) + self.write_u8(0x30, 0) + + @property + def coding_rate(self) -> Literal[5, 6, 7, 8]: + """The coding rate used by the radio to control forward error + correction (try setting to a higher value to increase tolerance of + short bursts of interference or to a lower value to increase bit + rate). Valid values are limited to 5, 6, 7, or 8.""" + cr_id = (self.read_u8(_RF95_REG_1D_MODEM_CONFIG1) & 0x0E) >> 1 + denominator = cr_id + 4 + return denominator + + @coding_rate.setter + def coding_rate(self, val: Literal[5, 6, 7, 8]) -> None: + # Set coding rate (set to 5 to match RadioHead Cr45). + denominator = min(max(val, 5), 8) + cr_id = denominator - 4 + self.write_u8( + _RF95_REG_1D_MODEM_CONFIG1, + (self.read_u8(_RF95_REG_1D_MODEM_CONFIG1) & 0xF1) | (cr_id << 1), + ) + + @property + def spreading_factor(self) -> Literal[6, 7, 8, 9, 10, 11, 12]: + """The spreading factor used by the radio (try setting to a higher + value to increase the receiver's ability to distinguish signal from + noise or to a lower value to increase the data transmission rate). + Valid values are limited to 6, 7, 8, 9, 10, 11, or 12.""" + sf_id = (self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) & 0xF0) >> 4 + return sf_id + + @spreading_factor.setter + def spreading_factor(self, val: Literal[6, 7, 8, 9, 10, 11, 12]) -> None: + # Set spreading factor (set to 7 to match RadioHead Sf128). + val = min(max(val, 6), 12) + + if val == 6: + self.detection_optimize = 0x5 + else: + self.detection_optimize = 0x3 + + self.write_u8(_RF95_DETECTION_THRESHOLD, 0x0C if val == 6 else 0x0A) + self.write_u8( + _RF95_REG_1E_MODEM_CONFIG2, + ((self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) & 0x0F) | ((val << 4) & 0xF0)), + ) + + @property + def enable_crc(self) -> bool: + """Set to True to enable hardware CRC checking of incoming packets. + Incoming packets that fail the CRC check are not processed. Set to + False to disable CRC checking and process all incoming packets.""" + return (self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) & 0x04) == 0x04 + + @enable_crc.setter + def enable_crc(self, val: bool) -> None: + # Optionally enable CRC checking on incoming packets. + if val: + self.write_u8( + _RF95_REG_1E_MODEM_CONFIG2, + self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) | 0x04, + ) + else: + self.write_u8( + _RF95_REG_1E_MODEM_CONFIG2, + self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) & 0xFB, + ) + + @property + def crc_error(self) -> bool: + """crc status""" + return (self.read_u8(_RF95_REG_12_IRQ_FLAGS) & 0x20) >> 5 + + def packet_sent(self) -> bool: + """Transmit status""" + return (self.read_u8(_RF95_REG_12_IRQ_FLAGS) & 0x8) >> 3 + + def payload_ready(self) -> bool: + """Receive status""" + return (self.read_u8(_RF95_REG_12_IRQ_FLAGS) & 0x40) >> 6 + + def clear_interrupt(self) -> None: + """Clear Interrupt flags""" + self.write_u8(_RF95_REG_12_IRQ_FLAGS, 0xFF) + + def fill_fifo(self, payload: ReadableBuffer) -> None: + """len_data is not used but is here for compatibility with rfm69 + Fill the FIFO with a packet to send""" + self.write_u8(_RF95_REG_0D_FIFO_ADDR_PTR, 0x00) # FIFO starts at 0. + # Write payload. + self.write_from(_RF95_REG_00_FIFO, payload) + # Write payload and header length. + self.write_u8(_RF95_REG_22_PAYLOAD_LENGTH, len(payload)) + + def read_fifo(self) -> bytearray: + """Read the data from the FIFO.""" + # Read the length of the FIFO. + fifo_length = self.read_u8(_RF95_REG_13_RX_NB_BYTES) + if fifo_length > 0: # read and clear the FIFO if anything in it + packet = bytearray(fifo_length) + current_addr = self.read_u8(_RF95_REG_10_FIFO_RX_CURRENT_ADDR) + self.write_u8(_RF95_REG_0D_FIFO_ADDR_PTR, current_addr) + # read the packet + self.read_into(_RF95_REG_00_FIFO, packet) + + # clear interrupt + self.write_u8(_RF95_REG_12_IRQ_FLAGS, 0xFF) + return packet diff --git a/FC_Board/lib/adafruit_rfm/rfm9xfsk.py b/FC_Board/lib/adafruit_rfm/rfm9xfsk.py new file mode 100644 index 00000000..f46df761 --- /dev/null +++ b/FC_Board/lib/adafruit_rfm/rfm9xfsk.py @@ -0,0 +1,578 @@ +# SPDX-FileCopyrightText: 2024 Jerry Needell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_rfm.rfm9xfsk` +==================================================== + +CircuitPython module for the RFM95/6/7/8 FSK 433/915mhz radio modules. + +* Author(s): Jerry Needell +""" + +import time + +from micropython import const + +from adafruit_rfm.rfm_common import RFMSPI + +try: + from typing import Optional + + import busio + import digitalio + from circuitpython_typing import ReadableBuffer + + try: + from typing import Literal + except ImportError: + from typing_extensions import Literal + +except ImportError: + pass + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RFM.git" + +# pylint: disable=duplicate-code + +# Internal constants: +# Register names (FSK Mode even though we use LoRa instead, from table 85) +_RF95_REG_00_FIFO = const(0x00) +_RF95_REG_01_OP_MODE = const(0x01) +_RF95_REG_02_BITRATE_MSB = const(0x02) +_RF95_REG_03_BITRATE_LSB = const(0x03) +_RF95_REG_04_FDEV_MSB = const(0x4) +_RF95_REG_05_FDEV_LSB = const(0x5) +_RF95_REG_06_FRF_MSB = const(0x06) +_RF95_REG_07_FRF_MID = const(0x07) +_RF95_REG_08_FRF_LSB = const(0x08) +_RF95_REG_09_PA_CONFIG = const(0x09) +_RF95_REG_0A_PA_RAMP = const(0x0A) +_RF95_REG_0B_OCP = const(0x0B) +_RF95_REG_0C_LNA = const(0x0C) +_RF95_REG_0D_RX_CFG = const(0x0D) +_RF95_REG_0E_RSSI_CFG = const(0x0E) +_RF95_REG_0F_RSSI_COLLISION = const(0x0F) +_RF95_REG_10_RSSI_THRESH = const(0x10) +_RF95_REG_11_RSSI_VALUE = const(0x11) +_RF95_REG_12_RX_BW = const(0x12) +_RF95_REG_13_AFC_BW = const(0x13) +_RF95_REG_14_OOK_PEAK = const(0x14) +_RF95_REG_15_OOK_FIX = const(0x15) +_RF95_REG_16_OOK_AVG = const(0x16) +_RF95_REG_1A_AFC_FEI_CTL = const(0x1A) +_RF95_REG_1B_AFC_MSB = const(0x1B) +_RF95_REG_1C_AFC_LSB = const(0x1C) +_RF95_REG_1D_FEI_MSB = const(0x1D) +_RF95_REG_1E_FEI_LSB = const(0x1E) +_RF95_REG_1F_PREAMBLE_DETECT = const(0x1F) +_RF95_REG_20_RX_TIMEOUT_1 = const(0x20) +_RF95_REG_21_RX_TIMEOUT_2 = const(0x21) +_RF95_REG_22_RX_TIMEOUT_3 = const(0x22) +_RF95_REG_23_RX_DELAY = const(0x23) +_RF95_REG_24_OSC = const(0x24) +_RF95_REG_25_PREAMBLE_MSB = const(0x25) +_RF95_REG_26_PREAMBLE_LSB = const(0x26) +_RF95_REG_27_SYNC_CONFIG = const(0x27) +_RF95_REG_28_SYNC_VALUE_1 = const(0x28) +_RF95_REG_29_SYNC_VALUE_2 = const(0x29) +_RF95_REG_2A_SYNC_VALUE_3 = const(0x2A) +_RF95_REG_2B_SYNC_VALUE_4 = const(0x2B) +_RF95_REG_2C_SYNC_VALUE_5 = const(0x2C) +_RF95_REG_2D_SYNC_VALUE_6 = const(0x2D) +_RF95_REG_2E_SYNC_VALUE_7 = const(0x2E) +_RF95_REG_2F_SYNC_VALUE_8 = const(0x2F) +_RF95_REG_30_PACKET_CONFIG_1 = const(0x30) +_RF95_REG_31_PACKET_CONFIG_2 = const(0x31) +_RF95_REG_32_PAYLOAD_LENGTH = const(0x32) +_RF95_REG_33_NODE_ADDR = const(0x33) +_RF95_REG_34_BROADCAST_ADDR = const(0x34) +_RF95_REG_35_FIFO_THRESH = const(0x35) +_RF95_REG_36_SEQ_CFG_1 = const(0x36) +_RF95_REG_37_SEQ_CFG_2 = const(0x37) +_RF95_REG_38_TIMER_RES = const(0x38) +_RF95_REG_39_TIMER1_COEF = const(0x39) +_RF95_REG_3A_TIMER2_COEF = const(0x3A) +_RF95_REG_3B_IMAGE_CAL = const(0x3B) +_RF95_REG_3C_TEMP = const(0x3C) +_RF95_REG_3D_LOW_BAT = const(0x3D) +_RF95_REG_3E_IRQ_FLAGS_1 = const(0x3E) +_RF95_REG_3F_IRQ_FLAGS_2 = const(0x3F) + +_RF95_REG_40_DIO_MAPPING1 = const(0x40) +_RF95_REG_41_DIO_MAPPING2 = const(0x41) +_RF95_REG_42_VERSION = const(0x42) + +_RF95_REG_44_PIII_IOP = const(0x44) + +_RF95_REG_4B_TCXO = const(0x4B) +_RF95_REG_4D_PA_DAC = const(0x4D) +_RF95_REG_5B_FORMER_TEMP = const(0x5B) +_RF95_REG_5B_BIT_RATE_FRAC = const(0x5D) +_RF95_REG_61_AGC_REF = const(0x61) +_RF95_REG_62_AGC_THRESH1 = const(0x62) +_RF95_REG_63_AGC_THRESH2 = const(0x63) +_RF95_REG_64_AGC_THRESH3 = const(0x64) + + +_RF95_PA_DAC_DISABLE = const(0x04) +_RF95_PA_DAC_ENABLE = const(0x07) + +# The crystal oscillator frequency of the module +_RF95_FXOSC = 32000000.0 + +# The Frequency Synthesizer step = RH_RF95_FXOSC / 2^^19 +_RF95_FSTEP = _RF95_FXOSC / 524288 + +# RadioHead specific compatibility constants. +_RH_BROADCAST_ADDRESS = const(0xFF) + +# The acknowledgement bit in the FLAGS +# The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved +# for application layer use. +_RH_FLAGS_ACK = const(0x80) +_RH_FLAGS_RETRY = const(0x40) + +# User facing constants: +SLEEP_MODE = 0b000 +STANDBY_MODE = 0b001 +FS_TX_MODE = 0b010 +TX_MODE = 0b011 +FS_RX_MODE = 0b100 +RX_MODE = 0b101 + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-public-methods +class RFM9xFSK(RFMSPI): + """Interface to a RFM95/6/7/8 FSK radio module. Allows sending and + receiving bytes of data in FSK mode at a support board frequency + (433/915mhz). + + :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 frequency: The center frequency to configure for radio transmission and reception. + Must be a frequency supported by your hardware (i.e. either 433 or 915mhz). + :param bytes sync_word: A byte string up to 8 bytes long which represents the syncronization + word used by received and transmitted packets. Read the datasheet for a full understanding + of this value! However by default the library will set a value that matches the RadioHead + Arduino library. + :param int preamble_length: The number of bytes to pre-pend to a data packet as a preamble. + This is by default 4 to match the RadioHead library. + :param bool high_power: Indicate if the chip is a high power variant that supports boosted + transmission power. The default is True as it supports the common RFM69HCW modules sold by + Adafruit. + + Also note this library tries to be compatible with raw RadioHead Arduino + library communication. This means the library sets up the radio modulation + to match RadioHead's defaults. + Advanced RadioHead features like address/node specific packets + or "reliable datagram" delivery are supported however due to the + limitations noted, "reliable datagram" is still subject to missed packets. + """ + + operation_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, bits=3) + low_frequency_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=3, bits=1) + modulation_type = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=5, bits=2) + modulation_shaping = RFMSPI.RegisterBits(_RF95_REG_0A_PA_RAMP, offset=5, bits=2) + # Long range/LoRa mode can only be set in sleep mode! + long_range_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=7, bits=1) + sync_on = RFMSPI.RegisterBits(_RF95_REG_27_SYNC_CONFIG, offset=4, bits=1) + sync_size = RFMSPI.RegisterBits(_RF95_REG_27_SYNC_CONFIG, offset=0, bits=3) + output_power = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, bits=4) + max_power = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, offset=4, bits=3) + pa_select = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, offset=7, bits=1) + pa_dac = RFMSPI.RegisterBits(_RF95_REG_4D_PA_DAC, bits=3) + dio0_mapping = RFMSPI.RegisterBits(_RF95_REG_40_DIO_MAPPING1, offset=6, bits=2) + lna_boost_hf = RFMSPI.RegisterBits(_RF95_REG_0C_LNA, offset=0, bits=2) + rx_bw_mantissa = RFMSPI.RegisterBits(_RF95_REG_12_RX_BW, offset=3, bits=2) + rx_bw_exponent = RFMSPI.RegisterBits(_RF95_REG_12_RX_BW, offset=0, bits=3) + afc_bw_mantissa = RFMSPI.RegisterBits(_RF95_REG_13_AFC_BW, offset=3, bits=2) + afc_bw_exponent = RFMSPI.RegisterBits(_RF95_REG_13_AFC_BW, offset=0, bits=3) + packet_format = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=7, bits=1) + dc_free = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=5, bits=2) + crc_on = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=4, bits=1) + crc_auto_clear_off = RFMSPI.RegisterBits( + _RF95_REG_30_PACKET_CONFIG_1, offset=3, bits=1 + ) + address_filter = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=1, bits=2) + crc_type = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=0, bits=1) + mode_ready = RFMSPI.RegisterBits(_RF95_REG_3E_IRQ_FLAGS_1, offset=7) + ook_bit_sync_on = RFMSPI.RegisterBits(_RF95_REG_14_OOK_PEAK, offset=5, bits=1) + ook_thresh_type = RFMSPI.RegisterBits(_RF95_REG_14_OOK_PEAK, offset=4, bits=2) + ook_thresh_step = RFMSPI.RegisterBits(_RF95_REG_14_OOK_PEAK, offset=0, bits=3) + ook_peak_thresh_dec = RFMSPI.RegisterBits(_RF95_REG_16_OOK_AVG, offset=5, bits=3) + ook_average_offset = RFMSPI.RegisterBits(_RF95_REG_16_OOK_AVG, offset=2, bits=2) + ook_average_thresh_filt = RFMSPI.RegisterBits( + _RF95_REG_16_OOK_AVG, offset=0, bits=2 + ) + + def __init__( # noqa: PLR0913 + self, + spi: busio.SPI, + cs: digitalio.DigitalInOut, # pylint: disable=invalid-name + rst: digitalio.DigitalInOut, + frequency: int, + *, + sync_word: bytes = b"\x2d\xd4", + preamble_length: int = 4, + high_power: bool = True, + baudrate: int = 5000000, + crc: bool = True, + ) -> None: + super().__init__(spi, cs, baudrate=baudrate) + self.module = "RFM9X" + self.max_packet_length = 252 + self.high_power = high_power + # Device support SPI mode 0 (polarity & phase = 0) up to a max of 10mhz. + # Set Default Baudrate to 5MHz to avoid problems + # self._device = spidev.SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=0) + # Setup reset as a digital output - initially High + # This line is pulled low as an output quickly to trigger a reset. + self._rst = rst + # initialize Reset High + self._rst.switch_to_output(value=True) + self.reset() + # No device type check! Catch an error from the very first request and + # throw a nicer message to indicate possible wiring problems. + version = self.read_u8(address=_RF95_REG_42_VERSION) + if version != 18: + raise RuntimeError( + "Failed to find rfm9x with expected version -- check wiring. Version found:", + hex(version), + ) + + # Set sleep mode, wait 10s and confirm in sleep mode (basic device check). + # Also set long range mode (LoRa mode) as it can only be done in sleep. + self.sleep() + time.sleep(0.01) + self.long_range_mode = False + if self.operation_mode != SLEEP_MODE or self.long_range_mode: + raise RuntimeError("Failed to configure radio for FSK mode, check wiring!") + # clear default setting for access to LF registers if frequency > 525MHz + if frequency > 525: + self.low_frequency_mode = 0 + # Set mode idle + self.idle() + # Setup the chip in a similar way to the RadioHead RFM69 library. + # Set FIFO TX condition to not empty and the default FIFO threshold to 15. + self.write_u8(_RF95_REG_35_FIFO_THRESH, 0b10001111) + # Set the syncronization word. + self.sync_word = sync_word + self.preamble_length = preamble_length # Set the preamble length. + self.frequency_mhz = frequency # Set frequency. + # Configure modulation for RadioHead library GFSK_Rb250Fd250 mode + # by default. Users with advanced knowledge can manually reconfigure + # for any other mode (consulting the datasheet is absolutely + # necessary!). + self.modulation_shaping = 0b01 # Gaussian filter, BT=1.0 + self.bitrate = 250000 # 250kbs + self.frequency_deviation = 250000 # 250khz + self.rx_bw_mantissa = 0b00 + self.rx_bw_exponent = 0b000 + self.afc_bw_mantissa = 0b00 + self.afc_bw_exponent = 0b000 + self.packet_format = 1 # Variable length. + self.dc_free = 0b10 # Whitening + # Set transmit power to 13 dBm, a safe value any module supports. + self._tx_power = 13 + self.tx_power = self._tx_power + + # Default to enable CRC checking on incoming packets. + self.enable_crc = crc + """CRC Enable state""" + self.snr = None + + def reset(self) -> None: + """Perform a reset of the chip.""" + # See section 7.2.2 of the datasheet for reset description. + self._rst.value = False # Set Reset Low + time.sleep(0.0001) # 100 us + self._rst.value = True # set Reset High + time.sleep(0.005) # 5 ms + + def idle(self) -> None: + """Enter idle standby mode.""" + self.operation_mode = STANDBY_MODE + + def sleep(self) -> None: + """Enter sleep mode.""" + self.operation_mode = SLEEP_MODE + + def listen(self) -> None: + """Listen for packets to be received by the chip. Use :py:func:`receive` + to listen, wait and retrieve packets as they're available. + """ + self.operation_mode = RX_MODE + self.dio0_mapping = 0b00 # Interrupt on rx done. + + def transmit(self) -> None: + """Transmit a packet which is queued in the FIFO. This is a low level + function for entering transmit mode and more. For generating and + transmitting a packet of data use :py:func:`send` instead. + """ + self.operation_mode = TX_MODE + self.dio0_mapping = 0b00 # Interrupt on tx done. + + @property + def sync_word(self) -> bytearray: + """The synchronization word value. This is a byte string up to 8 bytes long (64 bits) + which indicates the synchronization word for transmitted and received packets. Any + received packet which does not include this sync word will be ignored. The default value + is 0x2D, 0xD4 which matches the RadioHead RFM69 library. Setting a value of None will + disable synchronization word matching entirely. + """ + # Handle when sync word is disabled.. + if not self.sync_on: + return None + # Sync word is not disabled so read the current value. + sync_word_length = self.sync_size + 1 # Sync word size is offset by 1 + # according to datasheet. + sync_word = bytearray(sync_word_length) + self.read_into(_RF95_REG_28_SYNC_VALUE_1, sync_word) + return sync_word + + @sync_word.setter + def sync_word(self, val: Optional[bytearray]) -> None: + # Handle disabling sync word when None value is set. + if val is None: + self.sync_on = 0 + else: + # Check sync word is at most 8 bytes. + assert 1 <= len(val) <= 8 + # Update the value, size and turn on the sync word. + self.write_from(_RF95_REG_28_SYNC_VALUE_1, val) + self.sync_size = len(val) - 1 # Again sync word size is offset by + # 1 according to datasheet. + self.sync_on = 1 + + @property + def bitrate(self) -> float: + """The modulation bitrate in bits/second (or chip rate if Manchester encoding is enabled). + Can be a value from ~489 to 32mbit/s, but see the datasheet for the exact supported + values. + """ + msb = self.read_u8(_RF95_REG_02_BITRATE_MSB) + lsb = self.read_u8(_RF95_REG_03_BITRATE_LSB) + return _RF95_FXOSC / ((msb << 8) | lsb) + + @bitrate.setter + def bitrate(self, val: float) -> None: + assert (_RF95_FXOSC / 65535) <= val <= 32000000.0 + # Round up to the next closest bit-rate value with addition of 0.5. + bitrate = int((_RF95_FXOSC / val) + 0.5) & 0xFFFF + self.write_u8(_RF95_REG_02_BITRATE_MSB, bitrate >> 8) + self.write_u8(_RF95_REG_03_BITRATE_LSB, bitrate & 0xFF) + + @property + def frequency_deviation(self) -> float: + """The frequency deviation in Hertz.""" + msb = self.read_u8(_RF95_REG_04_FDEV_MSB) + lsb = self.read_u8(_RF95_REG_05_FDEV_LSB) + return _RF95_FSTEP * ((msb << 8) | lsb) + + @frequency_deviation.setter + def frequency_deviation(self, val: float) -> None: + assert 0 <= val <= (_RF95_FSTEP * 16383) # fdev is a 14-bit unsigned value + # Round up to the next closest integer value with addition of 0.5. + fdev = int((val / _RF95_FSTEP) + 0.5) & 0x3FFF + self.write_u8(_RF95_REG_04_FDEV_MSB, fdev >> 8) + self.write_u8(_RF95_REG_05_FDEV_LSB, fdev & 0xFF) + + @property + def temperature(self) -> float: + """The internal temperature of the chip.. See Sec 5.5.7 of the DataSheet + calibrated or very accurate. + """ + temp = self.read_u8(_RF95_REG_3C_TEMP) + return temp + + @property + def preamble_length(self) -> int: + """The length of the preamble for sent and received packets, an unsigned + 16-bit value. Received packets must match this length or they are + ignored! Set to 4 to match the RF69. + """ + msb = self.read_u8(_RF95_REG_25_PREAMBLE_MSB) + lsb = self.read_u8(_RF95_REG_26_PREAMBLE_LSB) + return ((msb << 8) | lsb) & 0xFFFF + + @preamble_length.setter + def preamble_length(self, val: int) -> None: + assert 0 <= val <= 65535 + self.write_u8(_RF95_REG_25_PREAMBLE_MSB, (val >> 8) & 0xFF) + self.write_u8(_RF95_REG_26_PREAMBLE_LSB, val & 0xFF) + + @property + def frequency_mhz(self) -> Literal[433.0, 915.0]: + """The frequency of the radio in Megahertz. Only the allowed values for + your radio must be specified (i.e. 433 vs. 915 mhz)! + """ + msb = self.read_u8(_RF95_REG_06_FRF_MSB) + mid = self.read_u8(_RF95_REG_07_FRF_MID) + lsb = self.read_u8(_RF95_REG_08_FRF_LSB) + frf = ((msb << 16) | (mid << 8) | lsb) & 0xFFFFFF + frequency = (frf * _RF95_FSTEP) / 1000000.0 + return frequency + + @frequency_mhz.setter + def frequency_mhz(self, val: Literal[433.0, 915.0]) -> None: + if val < 240 or val > 960: + raise RuntimeError("frequency_mhz must be between 240 and 960") + # Calculate FRF register 24-bit value. + frf = int((val * 1000000.0) / _RF95_FSTEP) & 0xFFFFFF + # Extract byte values and update registers. + msb = frf >> 16 + mid = (frf >> 8) & 0xFF + lsb = frf & 0xFF + self.write_u8(_RF95_REG_06_FRF_MSB, msb) + self.write_u8(_RF95_REG_07_FRF_MID, mid) + self.write_u8(_RF95_REG_08_FRF_LSB, lsb) + + @property + def tx_power(self) -> int: + """The transmit power in dBm. Can be set to a value from 5 to 23 for + high power devices (RFM95/96/97/98, high_power=True) or -1 to 14 for low + power devices. Only integer power levels are actually set (i.e. 12.5 + will result in a value of 12 dBm). + The actual maximum setting for high_power=True is 20dBm but for values > 20 + the PA_BOOST will be enabled resulting in an additional gain of 3dBm. + The actual setting is reduced by 3dBm. + The reported value will reflect the reduced setting. + """ + if self.high_power: + return self.output_power + 5 + return self.output_power - 1 + + @tx_power.setter + def tx_power(self, val: int) -> None: + val = int(val) + if self.high_power: + if val < 5 or val > 23: + raise RuntimeError("tx_power must be between 5 and 23") + # Enable power amp DAC if power is above 20 dB. + # Lower setting by 3db when PA_BOOST enabled - see Data Sheet Section 6.4 + if val > 20: + self.pa_dac = _RF95_PA_DAC_ENABLE + val -= 3 + else: + self.pa_dac = _RF95_PA_DAC_DISABLE + self.pa_select = True + self.output_power = (val - 5) & 0x0F + else: + assert -1 <= val <= 14 + self.pa_select = False + self.max_power = 0b111 # Allow max power output. + self.output_power = (val + 1) & 0x0F + + @property + def rssi(self) -> float: + """The received strength indicator (in dBm) of the last received message.""" + # Read RSSI register and convert to value using formula in datasheet. + # Remember in LoRa mode the payload register changes function to RSSI! + raw_rssi = self.read_u8(_RF95_REG_11_RSSI_VALUE) + return -raw_rssi / 2.0 + + @property + def enable_crc(self) -> bool: + """Set to True to enable hardware CRC checking of incoming packets. + Incoming packets that fail the CRC check are not processed. Set to + False to disable CRC checking and process all incoming packets.""" + return self.crc_on + + @enable_crc.setter + def enable_crc(self, val: bool) -> None: + # Optionally enable CRC checking on incoming packets. + if val: + self.crc_on = 1 + self.crc_type = 0 # use CCITT for RF69 compatibility + else: + self.crc_on = 0 + + @property + def crc_error(self) -> bool: + """crc status""" + return (self.read_u8(_RF95_REG_3F_IRQ_FLAGS_2) & 0x2) >> 1 + + @property + def enable_address_filter(self) -> bool: + """Set to True to enable address filtering. + Incoming packets that do no match the node address or broadcast address + will be ignored.""" + return self.address_filter + + @enable_address_filter.setter + def enable_address_filter(self, val: bool) -> None: + # Enable address filtering on incoming packets. + if val: + self.address_filter = 2 # accept node address or broadcast address + else: + self.address_filter = 0 + + @property + def fsk_node_address(self) -> int: + """Node Address for Address Filtering""" + return self.read_u8(_RF95_REG_33_NODE_ADDR) + + @fsk_node_address.setter + def fsk_node_address(self, val: int) -> None: + assert 0 <= val <= 255 + self.write_u8(_RF95_REG_33_NODE_ADDR, val) + + @property + def fsk_broadcast_address(self) -> int: + """Node Address for Address Filtering""" + return self.read_u8(_RF95_REG_34_BROADCAST_ADDR) + + @fsk_broadcast_address.setter + def fsk_broadcast_address(self, val: int) -> None: + assert 0 <= val <= 255 + self.write_u8(_RF95_REG_34_BROADCAST_ADDR, val) + + @property + def ook_fixed_threshold(self) -> int: + """Fixed threshold for data slicer in OOK mode""" + return self.read_u8(_RF95_REG_15_OOK_FIX) + + @ook_fixed_threshold.setter + def ook_fixed_threshold(self, val: int) -> None: + assert 0 <= val <= 255 + self.write_u8(_RF95_REG_15_OOK_FIX, val) + + def packet_sent(self) -> bool: + """Transmit status""" + return (self.read_u8(_RF95_REG_3F_IRQ_FLAGS_2) & 0x8) >> 3 + + def payload_ready(self) -> bool: + """Receive status""" + return (self.read_u8(_RF95_REG_3F_IRQ_FLAGS_2) & 0x4) >> 2 + + def clear_interrupt(self) -> None: + """Clear interrupt Flags""" + self.write_u8(_RF95_REG_3E_IRQ_FLAGS_1, 0xFF) + self.write_u8(_RF95_REG_3F_IRQ_FLAGS_2, 0xFF) + + def fill_fifo(self, payload: ReadableBuffer) -> None: + """Write the payload to the FIFO.""" + complete_payload = bytearray(1) # prepend packet length to payload + complete_payload[0] = len(payload) + # put the payload lengthe in the beginning of the packet for RFM69 + complete_payload = complete_payload + payload + # Write payload to transmit fifo + self.write_from(_RF95_REG_00_FIFO, complete_payload) + + def read_fifo(self) -> bytearray: + """Read the data from the FIFO.""" + # Read the length of the FIFO. + fifo_length = self.read_u8(_RF95_REG_00_FIFO) + if fifo_length > 0: # read and clear the FIFO if anything in it + packet = bytearray(fifo_length) + # read the packet + self.read_into(_RF95_REG_00_FIFO, packet, fifo_length) + return packet diff --git a/FC_Board/lib/adafruit_rfm/rfm_common.py b/FC_Board/lib/adafruit_rfm/rfm_common.py new file mode 100644 index 00000000..34d607b6 --- /dev/null +++ b/FC_Board/lib/adafruit_rfm/rfm_common.py @@ -0,0 +1,586 @@ +# SPDX-FileCopyrightText: 2024 Jerry Needell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" + +* Author(s): Jerry Needell +""" + +import asyncio +import random +import time + +from adafruit_bus_device import spi_device + +try: + from typing import Callable, Optional, Type + + import busio + import digitalio + from circuitpython_typing import ReadableBuffer, WriteableBuffer + +except ImportError: + pass + +from micropython import const + +HAS_SUPERVISOR = False + +try: + import supervisor + + if hasattr(supervisor, "ticks_ms"): + HAS_SUPERVISOR = True +except ImportError: + pass + + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RFM.git" + + +# RadioHead specific compatibility constants. +_RH_BROADCAST_ADDRESS = const(0xFF) + +# The acknowledgement bit in the FLAGS +# The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved +# for application layer use. +_RH_FLAGS_ACK = const(0x80) +_RH_FLAGS_RETRY = const(0x40) + +_RH_RF95_REG_5B_FORMER_TEMP = const(0x5B) + +# supervisor.ticks_ms() contants +_TICKS_PERIOD = const(1 << 29) +_TICKS_MAX = const(_TICKS_PERIOD - 1) +_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2) + + +def ticks_diff(ticks1: int, ticks2: int) -> int: + """Compute the signed difference between two ticks values + assuming that they are within 2**28 ticks + """ + diff = (ticks1 - ticks2) & _TICKS_MAX + diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD + return diff + + +def asyncio_to_blocking(function): + """run async function as normal blocking function""" + + def blocking_function(self, *args, **kwargs): + return asyncio.run(function(self, *args, **kwargs)) + + return blocking_function + + +async def asyncio_check_timeout( + flag: Callable, limit: float, timeout_poll: float +) -> bool: + """test for timeout waiting for specified flag""" + timed_out = False + if HAS_SUPERVISOR: + start = supervisor.ticks_ms() + while not timed_out and not flag(): + if ticks_diff(supervisor.ticks_ms(), start) >= limit * 1000: + timed_out = True + await asyncio.sleep(timeout_poll) + else: + start = time.monotonic() + while not timed_out and not flag(): + if time.monotonic() - start >= limit: + timed_out = True + await asyncio.sleep(timeout_poll) + + return timed_out + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-nested-blocks +class RFMSPI: + """Base class for SPI type devices""" + + class RegisterBits: + """Simplify register access""" + + # Class to simplify access to the many configuration bits avaialable + # on the chip's registers. This is a subclass here instead of using + # a higher level module to increase the efficiency of memory usage + # (all of the instances of this bit class will share the same buffer + # used by the parent RFM69 class instance vs. each having their own + # buffer and taking too much memory). + + # Quirk of pylint that it requires public methods for a class. This + # is a decorator class in Python and by design it has no public methods. + # Instead it uses dunder accessors like get and set below. For some + # reason pylint can't figure this out so disable the check. + # pylint: disable=too-few-public-methods + + # Again pylint fails to see the true intent of this code and warns + # against private access by calling the write and read functions below. + # This is by design as this is an internally used class. Disable the + # check from pylint. + # pylint: disable=protected-access + + def __init__(self, address: int, *, offset: int = 0, bits: int = 1) -> None: + assert 0 <= offset <= 7 + assert 1 <= bits <= 8 + assert (offset + bits) <= 8 + self._address = address + self._mask = 0 + for _ in range(bits): + self._mask <<= 1 + self._mask |= 1 + self._mask <<= offset + self._offset = offset + + def __get__(self, obj: Optional["RFM"], objtype: Type["RFM"]) -> int: + reg_value = obj.read_u8(self._address) + return (reg_value & self._mask) >> self._offset + + def __set__(self, obj: Optional["RFM"], val: int) -> None: + reg_value = obj.read_u8(self._address) + reg_value &= ~self._mask + reg_value |= (val & 0xFF) << self._offset + obj.write_u8(self._address, reg_value) + + # pylint: disable-msg=too-many-arguments + def __init__( # noqa: PLR0913 + self, + spi: busio.SPI, + cs_pin: digitalio.DigitalInOut, + baudrate: int = 5000000, + polarity: int = 0, + phase: int = 0, + ): + self.spi_device = spi_device.SPIDevice( + spi, cs_pin, baudrate=baudrate, polarity=polarity, phase=phase + ) + # initialize last RSSI reading + self.last_rssi = 0.0 + """The RSSI of the last received packet. Stored when the packet was received. + The instantaneous RSSI value may not be accurate once the + operating mode has been changed. + """ + self.last_snr = 0.0 + """The SNR of the last received packet. Stored when the packet was received. + The instantaneous SNR value may not be accurate once the + operating mode has been changed. + """ + # initialize timeouts and delays delays + self.ack_wait = 0.1 + """The delay time before attempting a retry after not receiving an ACK""" + self.receive_timeout = 0.5 + """The amount of time to poll for a received packet. + If no packet is received, the returned packet will be None + """ + self.xmit_timeout = 2.0 + """The amount of time to wait for the HW to transmit the packet. + This is mainly used to prevent a hang due to a HW issue + """ + self.ack_retries = 5 + """The number of ACK retries before reporting a failure.""" + self.ack_delay = None + """The delay time before attemting to send an ACK. + If ACKs are being missed try setting this to .1 or .2. + """ + # initialize sequence number counter for reliabe datagram mode + self.sequence_number = 0 + # create seen Ids list + self.seen_ids = bytearray(256) + # initialize packet header + # node address - default is broadcast + self.node = _RH_BROADCAST_ADDRESS + """The default address of this Node. (0-255). + If not 255 (0xff) then only packets address to this node will be accepted. + First byte of the RadioHead header. + """ + # destination address - default is broadcast + self.destination = _RH_BROADCAST_ADDRESS + """The default destination address for packet transmissions. (0-255). + If 255 (0xff) then any receiving node should accept the packet. + Second byte of the RadioHead header. + """ + # ID - contains seq count for reliable datagram mode + self.identifier = 0 + """Automatically set to the sequence number when send_with_ack() used. + Third byte of the RadioHead header. + """ + # flags - identifies ack/reetry packet for reliable datagram mode + self.flags = 0 + """Upper 4 bits reserved for use by Reliable Datagram Mode. + Lower 4 bits may be used to pass information. + Fourth byte of the RadioHead header. + """ + self.radiohead = True + """Enable RadioHead compatibility""" + + self.crc_error_count = 0 + self.timeout_poll = 0.001 + + # pylint: enable-msg=too-many-arguments + + # Global buffer for SPI commands + _BUFFER = bytearray(4) + + # pylint: disable=no-member + # Reconsider pylint: disable when this can be tested + def read_into( + self, address: int, buf: WriteableBuffer, length: Optional[int] = None + ) -> None: + """Read a number of bytes from the specified address into the provided + buffer. If length is not specified (the default) the entire buffer + will be filled.""" + if length is None: + length = len(buf) + with self.spi_device as device: + self._BUFFER[0] = address & 0x7F # Strip out top bit to set 0 + # value (read). + device.write(self._BUFFER, end=1) + device.readinto(buf, end=length) + + def read_u8(self, address: int) -> int: + """Read a single byte from the provided address and return it.""" + self.read_into(address, self._BUFFER, length=1) + return self._BUFFER[0] + + def write_from( + self, address: int, buf: ReadableBuffer, length: Optional[int] = None + ) -> None: + """Write a number of bytes to the provided address and taken from the + provided buffer. If no length is specified (the default) the entire + buffer is written.""" + if length is None: + length = len(buf) + with self.spi_device as device: + self._BUFFER[0] = (address | 0x80) & 0xFF # Set top bit to 1 to + # indicate a write. + device.write(self._BUFFER, end=1) + device.write(buf, end=length) + + def write_u8(self, address: int, val: int) -> None: + """Write a byte register to the chip. Specify the 7-bit address and the + 8-bit value to write to that address.""" + with self.spi_device as device: + self._BUFFER[0] = ( + address | 0x80 + ) & 0xFF # Set top bit to 1 to indicate a write. + self._BUFFER[1] = val & 0xFF + device.write(self._BUFFER, end=2) + + # pylint: disable=too-many-branches + + async def asyncio_send( # noqa: PLR0912 PLR0913 + self, + data: ReadableBuffer, + *, + keep_listening: bool = False, + destination: Optional[int] = None, + node: Optional[int] = None, + identifier: Optional[int] = None, + flags: Optional[int] = None, + ) -> bool: + """Send a string of data using the transmitter. + You can only send 252 bytes at a time + (limited by chip's FIFO size and appended headers). + if the propert radiohead is True then this appends a 4 byte header + to be compatible with the RadioHead library. + The header defaults to using the initialized attributes: + (destination,node,identifier,flags) + It may be temporarily overidden via the kwargs - destination,node,identifier,flags. + Values passed via kwargs do not alter the attribute settings. + The keep_listening argument should be set to True if you want to start listening + automatically after the packet is sent. The default setting is False. + + Returns: True if success or False if the send timed out. + """ + # Disable pylint warning to not use length as a check for zero. + # This is a puzzling warning as the below code is clearly the most + # efficient and proper way to ensure a precondition that the provided + # buffer be within an expected range of bounds. Disable this check. + # pylint: disable=len-as-condition + assert 0 < len(data) <= self.max_packet_length + # pylint: enable=len-as-condition + self.idle() # Stop receiving to clear FIFO and keep it clear. + # Combine header and data to form payload + if self.radiohead: + payload = bytearray(4) + if destination is None: # use attribute + payload[0] = self.destination + else: # use kwarg + payload[0] = destination + if node is None: # use attribute + payload[1] = self.node + else: # use kwarg + payload[1] = node + if identifier is None: # use attribute + payload[2] = self.identifier + else: # use kwarg + payload[2] = identifier + if flags is None: # use attribute + payload[3] = self.flags + else: # use kwarg + payload[3] = flags + payload = payload + data + elif destination is not None: # prepend destination for non RH packets + payload = destination.to_bytes(1, "big") + data + else: + payload = data + self.fill_fifo(payload) + # Turn on transmit mode to send out the packet. + self.transmit() + # Wait for packet_sent interrupt with explicit polling (not ideal but + # best that can be done right now without interrupts). + timed_out = await asyncio_check_timeout( + self.packet_sent, self.xmit_timeout, self.timeout_poll + ) + # Listen again if necessary and return the result packet. + if keep_listening: + self.listen() + else: + # Enter idle mode to stop receiving other packets. + self.idle() + self.clear_interrupt() + return not timed_out + + send = asyncio_to_blocking(asyncio_send) + """Non-asyncio wrapper to Send a string of data using the transmitter + using the same arguments and keywords as asyncio_send() + """ + + async def asyncio_send_with_ack(self, data: ReadableBuffer) -> bool: + """Reliable Datagram mode: + Send a packet with data and wait for an ACK response. + The packet header is automatically generated. + If enabled, the packet transmission will be retried on failure + """ + if not self.radiohead: + raise RuntimeError("send_with_ack onl suppoted in RadioHead mode") + if self.ack_retries: + retries_remaining = self.ack_retries + else: + retries_remaining = 1 + got_ack = False + self.sequence_number = (self.sequence_number + 1) & 0xFF + while not got_ack and retries_remaining: + self.identifier = self.sequence_number + await self.asyncio_send(data, keep_listening=True) + # Don't look for ACK from Broadcast message + if self.destination == _RH_BROADCAST_ADDRESS: + got_ack = True + else: + # wait for a packet from our destination + ack_packet = await self.asyncio_receive( + timeout=self.ack_wait, with_header=True + ) + if ack_packet is not None: + if ack_packet[3] & _RH_FLAGS_ACK: + # check the ID + if ack_packet[2] == self.identifier: + got_ack = True + break + # pause before next retry -- random delay + if not got_ack: + # delay by random amount before next try + await asyncio.sleep(self.ack_wait + self.ack_wait * random.random()) + retries_remaining = retries_remaining - 1 + # set retry flag in packet header + self.flags |= _RH_FLAGS_RETRY + self.flags = 0 # clear flags + return got_ack + + send_with_ack = asyncio_to_blocking(asyncio_send_with_ack) + """Non-asyncio wrapper to Send a string of data using the transmitter + using the same arguments and keywords as asyncio_send_with_ack() + """ + + async def asyncio_receive( # noqa: PLR0912 + self, + *, + keep_listening: bool = True, + with_header: bool = False, + timeout: Optional[float] = None, + ) -> Optional[bytearray]: + """Wait to receive a packet from the receiver. If a packet is found the payload bytes + are returned, otherwise None is returned (which indicates the timeout elapsed with no + reception). + If keep_listening is True (the default) the chip will immediately enter listening mode + after reception of a packet, otherwise it will fall back to idle mode and ignore any + future reception. + Packets may have a 4-byte header for compatibility with the + RadioHead library. + The header consists of 4 bytes (To,From,ID,Flags). The default setting will strip + the header before returning the packet to the caller. + If with_header is True then the 4 byte header will be returned with the packet. + The payload then begins at packet[4]. + """ + if not self.radiohead and with_header: + raise RuntimeError("with_header only supported for RadioHead mode") + timed_out = False + if timeout is None: + timeout = self.receive_timeout + if timeout is not None: + # Wait for the payloadready signal. This is not ideal and will + # surely miss or overflow the FIFO when packets aren't read fast + # enough, however it's the best that can be done from Python without + # interrupt supports. + # Make sure we are listening for packets. + self.listen() + timed_out = await asyncio_check_timeout( + self.payload_ready, timeout, self.timeout_poll + ) + # Payload ready is set, a packet is in the FIFO. + packet = None + # save last RSSI reading + self.last_rssi = self.rssi + self.last_snr = self.snr + + # Enter idle mode to stop receiving other packets. + self.idle() + if not timed_out: + if self.enable_crc and self.crc_error: + self.crc_error_count += 1 + else: + packet = self.read_fifo() + if self.radiohead: + if len(packet) < 5: + # reject the packet if it is too small to contain the RAdioHead Header + packet = None + if packet is not None: + if ( + self.node != _RH_BROADCAST_ADDRESS # noqa: PLR1714 + and packet[0] != _RH_BROADCAST_ADDRESS + and packet[0] != self.node + ): + packet = None + if ( + not with_header and packet is not None + ): # skip the header if not wanted + packet = packet[4:] + # Listen again if necessary and return the result packet. + if keep_listening: + self.listen() + else: + # Enter idle mode to stop receiving other packets. + self.idle() + self.clear_interrupt() + return packet + + receive = asyncio_to_blocking(asyncio_receive) + """Non-asyncio wrapper to Receive a packet + using the same arguments and keywords as asyncio_receive() + """ + + async def asyncio_receive_with_ack( # noqa: PLR0912 + self, + *, + keep_listening: bool = True, + with_header: bool = False, + timeout: Optional[float] = None, + ) -> Optional[bytearray]: + """Wait to receive a RadioHead packet from the receiver then send an ACK packet in response. + AKA Reliable Datagram mode. + If a packet is found the payload bytes are returned, otherwise None is returned + (which indicates the timeout elapsed with no reception). + If keep_listening is True (the default) the chip will immediately enter listening mode + after receipt of a packet, otherwise it will fall back to idle mode and ignore + any incomming packets until it is called again. + All packets must have a 4-byte header for compatibility with the RadioHead library. + The header consists of 4 bytes (To,From,ID,Flags). The default setting will strip + the header before returning the packet to the caller. + If with_header is True then the 4 byte header will be returned with the packet. + The payload then begins at packet[4]. + """ + if not self.radiohead: + raise RuntimeError("receive_with_ack only supported for RadioHead mode") + timed_out = False + if timeout is None: + timeout = self.receive_timeout + if timeout is not None: + # Wait for the payloadready signal. This is not ideal and will + # surely miss or overflow the FIFO when packets aren't read fast + # enough, however it's the best that can be done from Python without + # interrupt supports. + # Make sure we are listening for packets. + self.listen() + timed_out = await asyncio_check_timeout( + self.payload_ready, timeout, self.timeout_poll + ) + # Payload ready is set, a packet is in the FIFO. + packet = None + # save last RSSI reading + self.last_rssi = self.rssi + self.last_snr = self.snr + + # Enter idle mode to stop receiving other packets. + self.idle() + if not timed_out: + if self.enable_crc and self.crc_error: + self.crc_error_count += 1 + else: + packet = self.read_fifo() + if self.radiohead: + if len(packet) < 5: + # reject the packet if it is too small to contain the RAdioHead Header + packet = None + if packet is not None: + if ( + self.node != _RH_BROADCAST_ADDRESS # noqa: PLR1714 + and packet[0] != _RH_BROADCAST_ADDRESS + and packet[0] != self.node + ): + packet = None + # send ACK unless this was an ACK or a broadcast + elif ((packet[3] & _RH_FLAGS_ACK) == 0) and ( + packet[0] != _RH_BROADCAST_ADDRESS + ): + # delay before sending Ack to give receiver a chance to get ready + if self.ack_delay is not None: + await asyncio.sleep(self.ack_delay) + # send ACK packet to sender (data is b'!') + await self.asyncio_send( + b"!", + destination=packet[1], + node=packet[0], + identifier=packet[2], + flags=(packet[3] | _RH_FLAGS_ACK), + ) + # reject Retries if we have seen this idetifier from this source before + if (self.seen_ids[packet[1]] == packet[2]) and ( + packet[3] & _RH_FLAGS_RETRY + ): + packet = None + else: # save the packet identifier for this source + self.seen_ids[packet[1]] = packet[2] + if ( + packet is not None and (packet[3] & _RH_FLAGS_ACK) != 0 + ): # Ignore it if it was an ACK packet + packet = None + if ( + not with_header and packet is not None + ): # skip the header if not wanted + packet = packet[4:] + # Listen again if necessary and return the result packet. + if keep_listening: + self.listen() + else: + # Enter idle mode to stop receiving other packets. + self.idle() + self.clear_interrupt() + return packet + + receive_with_ack = asyncio_to_blocking(asyncio_receive_with_ack) + """Non-asyncio wrapper to Receive a packet + using the same arguments and keywords as asyncio_receive_with_ack() + """ + + @property + def former_temperature(self): + """Tries to grab former temp from module""" + raw_temp = self.read_u8(_RH_RF95_REG_5B_FORMER_TEMP) + temp = raw_temp & 0x7F + if (raw_temp & 0x80) == 0x80: + temp = ~temp + 0x01 + + return temp + 143 # Added prescalar for temp diff --git a/Batt_Board/lib/adafruit_tca9548a.py b/FC_Board/lib/adafruit_tca9548a.py similarity index 97% rename from Batt_Board/lib/adafruit_tca9548a.py rename to FC_Board/lib/adafruit_tca9548a.py index c766176b..26a9cecc 100644 --- a/Batt_Board/lib/adafruit_tca9548a.py +++ b/FC_Board/lib/adafruit_tca9548a.py @@ -57,7 +57,9 @@ def __init__(self, tca: "TCA9548A", channel: int) -> None: def try_lock(self) -> bool: """Pass through for try_lock.""" - while not self.tca.i2c.try_lock(): + trys = 0 + while not self.tca.i2c.try_lock() and trys < 100: + trys += 1 time.sleep(0) self.tca.i2c.writeto(self.tca.address, self.channel_switch) return True diff --git a/FC_Board/lib/battery_helper.py b/FC_Board/lib/battery_helper.py new file mode 100644 index 00000000..710798f1 --- /dev/null +++ b/FC_Board/lib/battery_helper.py @@ -0,0 +1,293 @@ +import time + +# Written with Claude 3.5 +# Author: Michael Pham +# Date: 2024-11-05 + + +class BatteryHelper: + """Helper class for interfacing with PicoSquared battery management system""" + + # Command definitions + CMD_GET_TEMPERATURES = "1" # Returns thermocouple_temp, board_temp + CMD_GET_POWER = "2" # Returns battery_v, draw_i, charge_v, charge_i, is_charging + CMD_GET_ERRORS = "3" # Returns error_count, trust_memory + CMD_TOGGLE_FACES = "4" # Toggle face LEDs, returns face status + CMD_RESET_BUS = "5" # Reset power bus + CMD_TOGGLE_CAMERA = "6" # Toggle camera power, returns camera status + CMD_USE_AUX_RADIO = "7" # Switch to auxiliary radio + CMD_RESET_FC = "8" # Reset flight controller + CMD_BURN_COMPLETE = "9" # Set burn complete flag + CMD_RESET_MCU = "11" # Reset microcontroller + CMD_ERROR = "208" + + def __init__(self, pysquared): + """ + Initialize UART helper with existing Pysquared object + + Args: + pysquared: Pysquared object with initialized UART + """ + self.uart = pysquared.uart + self.last_command_time = 0 + self.debug_mode = True + + def _flush_input(self): + """Flush the input buffer""" + try_count = 0 + while self.uart.in_waiting and try_count < 10: + try_count += 1 + self.uart.read() + + def _wait_for_ack(self): + """Wait for acknowledgment character""" + start = time.monotonic() + + # Clear any existing data + if self.uart.in_waiting: + self.uart.read() + + # Wait up to 10ms for ACK + while (time.monotonic() - start) * 1000 < 10: + if self.uart.in_waiting: + byte = self.uart.read(1) + if self.debug_mode: + print(f"ACK byte received: {byte}") + if byte == b"A": + return True + time.sleep(0.001) + return False + + def _read_message(self, timeout_ms=150): + """Read until we get a complete message or timeout""" + + response = bytearray() + + # Initial wait for data + start = time.monotonic() + while ( + not self.uart.in_waiting and (time.monotonic() - start) * 1000 < timeout_ms + ): + pass + + # Read data as it comes + last_read = time.monotonic() + while (time.monotonic() - last_read) * 1000 < 5: # 5ms timeout between chunks + if self.uart.in_waiting: + response.extend(self.uart.read()) + last_read = time.monotonic() + + try: + text = response.decode("utf-8") + if self.debug_mode: + print(f"Buffer: {text}") + + # Check for complete message + if "AA<" in text and ">" in text: + start_idx = text.find("<") + end_idx = text.find(">") + if start_idx < end_idx: + return text[start_idx + 1 : end_idx] + except Exception as e: + print(f"Error decoding message: {e}") + pass + + return "" + + def _send_command(self, cmd): + """ + Send command and wait for acknowledgment + + Returns: + str: Response message or empty string on failure + """ + try: + + # Send command + self.uart.write(bytes(cmd.encode())) + + # Read the response message + return self._read_message() + + except Exception as e: + print(f"UART error: {e}") + return "" + + def _is_valid_message(self, msg): + """Verify message format and checksum if implemented""" + return bool(msg and len(msg) > 0) + + def get_temperatures(self): + """ + Get thermocouple and board temperatures + Returns: dict with 'thermocouple' and 'board' temperatures in degrees C + """ + response = self._send_command(self.CMD_GET_TEMPERATURES) + try: + thermo_temp, board_temp = map(float, response.split(",")) + return {"thermocouple": thermo_temp, "board": board_temp} + except (ValueError, AttributeError): + return None + + def get_power_metrics(self): + """ + Get power-related measurements + + Returns: + Tuple of (battery_voltage, draw_current, charge_voltage, + charge_current, is_charging, battery_percentage) + """ + response = self._send_command(self.CMD_GET_POWER) + + if response: + try: + parts = response.split(",") + if len(parts) == 5: + return ( + float(parts[0]), + float(parts[1]), + float(parts[2]), + float(parts[3]), + bool(int(parts[4])), + self.get_battery_percentage( + float(parts[0]), bool(int(parts[4])) + ), + ) + + except Exception as e: + if self.debug_mode: + print(f"Error parsing metrics: {e}") + + if self.debug_mode: + print("Failed to get valid power metrics") + return (0.0, 0.0, 0.0, 0.0, False, 0.0) + + def get_error_metrics(self): + """ + Get error count and trust memory + Returns: dict with error count and trust memory values + """ + response = self._send_command(self.CMD_GET_ERRORS) + try: + error_count, trust_memory = map(int, response.split(",")) + return {"error_count": error_count, "trust_memory": trust_memory} + except (ValueError, AttributeError): + return None + + def toggle_faces(self): + """Toggle face LEDs""" + return self._send_command(self.CMD_TOGGLE_FACES) + + def reset_power_bus(self): + """Reset the power bus""" + return self._send_command(self.CMD_RESET_BUS) + + def toggle_camera(self): + """Toggle camera power""" + return self._send_command(self.CMD_TOGGLE_CAMERA) + + def use_auxiliary_radio(self): + """Switch to auxiliary radio""" + return self._send_command(self.CMD_USE_AUX_RADIO) + + def reset_flight_controller(self): + """Reset the flight controller""" + return self._send_command(self.CMD_RESET_FC) + + def set_burn_complete(self): + """Set the burn complete flag""" + return self._send_command(self.CMD_BURN_COMPLETE) + + def reset_mcu(self): + """Reset the microcontroller""" + return self._send_command(self.CMD_RESET_MCU) + + def get_battery_percentage(self, pack_voltage, is_charging=False): + """ + Estimate remaining battery percentage for 2S LG MJ1 pack based on voltage. + Accounts for voltage rise during charging. + + Args: + pack_voltage (float): Current voltage of 2S battery pack + is_charging (bool): Whether the pack is currently being charged + + Returns: + float: Estimated remaining capacity percentage (0-100) + """ + # Convert pack voltage to cell voltage + cell_voltage = pack_voltage / 2 + + # Voltage compensation when charging (0.35V per cell = 0.7V per pack) + if is_charging: + cell_voltage = cell_voltage - 0.35 + + # Lookup table from 1A discharge curve [voltage, capacity_remaining_percent] + DISCHARGE_CURVE = [ + (4.2, 100), # Full charge + (4.0, 90), # Initial drop + (3.9, 80), + (3.8, 70), + (3.7, 60), + (3.6, 50), + (3.5, 40), + (3.4, 30), + (3.3, 20), + (3.2, 15), + (3.1, 10), + (3.0, 5), + (2.8, 2), + (2.7, 0), # Cutoff at 5.4V pack voltage + ] + + # Handle edge cases + if cell_voltage >= 4.2: + return 100 + if cell_voltage <= 2.7: + return 0 + + # Find the two voltage points our cell voltage falls between + for i in range(len(DISCHARGE_CURVE) - 1): + v1, p1 = DISCHARGE_CURVE[i] + v2, p2 = DISCHARGE_CURVE[i + 1] + + if v2 <= cell_voltage <= v1: + # Linear interpolation between points + percent = p2 + (p1 - p2) * (cell_voltage - v2) / (v1 - v2) + return round(percent, 1) + + return 0 # Fallback + + def debug_timing(self): + """Measure and print timing of each step""" + + print("\nTiming analysis:") + + # Measure command send time + start = time.monotonic() + self.uart.write(bytes(self.CMD_GET_POWER.encode())) + send_time = (time.monotonic() - start) * 1000 + + # Measure response read time + read_start = time.monotonic() + response = self._read_message() + read_time = (time.monotonic() - read_start) * 1000 + + # Measure parse time + parse_start = time.monotonic() + if response: + try: + parts = response.split(",") + values = [float(x) for x in parts[:4]] + values.append(bool(int(parts[4]))) + except Exception as e: + print(f"Parse error: {e}") + parse_time = (time.monotonic() - parse_start) * 1000 + + # Total time + total_time = (time.monotonic() - start) * 1000 + + print(f"Send time: {send_time:.2f}ms") + print(f"Read time: {read_time:.2f}ms") + print(f"Parse time: {parse_time:.2f}ms") + print(f"Total time: {total_time:.2f}ms") + print(f"Response: {response}") diff --git a/FC_Board/lib/can_bus_helper.py b/FC_Board/lib/can_bus_helper.py new file mode 100644 index 00000000..043f6d72 --- /dev/null +++ b/FC_Board/lib/can_bus_helper.py @@ -0,0 +1,462 @@ +import traceback +import time + +from adafruit_mcp2515.canio import ( + Message, + RemoteTransmissionRequest, +) # pylint: disable=import-error +from debugcolor import co # pylint: disable=import-error + +# There may be an AI induced error with the traceback statements + + +class CanBusHelper: + def __init__(self, can_bus, owner, debug): + self.can_bus = can_bus + self.owner = owner + self.debug = debug + self.multi_message_buffer = {} + self.current_id = 0x00 + self.MESSAGE_IDS = { + "BOOT_SEQUENCE": 0x01, + "CRITICAL_POWER_OPERATIONS": 0x02, + "LOW_POWER_OPERATIONS": 0x03, + "NORMAL_POWER_OPERATIONS": 0x04, + "FAULT_ID": 0x1A4, + "SOT_ID": 0xA5, + "EOT_ID": 0xA6, + # Add more message IDs as needed + } + + def debug_print(self, statement): + if self.debug: + print(co("[CAN_BUS][Communications]" + str(statement), "orange", "bold")) + + # BROKEN + def construct_messages(self, id, messages): + if not isinstance(messages, list): + messages = [messages] + message_objects = [] + sequence_number = 0 # Initialize sequence number + + for message in messages: + message = str(message) + byte_message = bytes(message, "UTF-8") + + for i in range(0, len(byte_message), 8): + chunk = byte_message[i : i + 8] + + if len(byte_message) > 8: + # Use the sequence number across all messages in the list + extended_id = ((id & 0x7F) << 22) | sequence_number + message_objects.append(Message(extended_id, chunk, extended=True)) + sequence_number += 1 # Increment sequence number for next chunk + else: + # For single message, keep the original ID + message_objects.append(Message(id, chunk)) + + return message_objects + + def send_can(self, id_str, data, timeout=5): + if id_str in self.MESSAGE_IDS: + id_byte = self.MESSAGE_IDS[id_str] + else: + # Handle the case where id_str is not in MESSAGE_IDS + raise ValueError(f"Invalid ID string: {id_str}") + + # Construct the messages + messages = self.construct_messages(id_byte, data) + + # Use SOT and EOT only for multi-message transmissions + if len(messages) > 1: + # Initiate handshake by sending SOT and waiting for ACK + if not self.send_sot(id_byte, len(messages)): + self.debug_print("Handshake failed: SOT not acknowledged") + return False + + # Send the messages and wait for ACK + if not self.send_messages(messages, timeout): + return False + + if len(messages) > 1: + # Send EOT after sending all messages + self.send_eot() + + return True + + def send_messages(self, messages, timeout=1): + """ + Sends the given messages and waits for acknowledgments. + """ + for i, message in enumerate(messages): + attempts = 0 + ack_received = False + while attempts < 3 and not ack_received: + try: + self.can_bus.send(message) + self.debug_print("Sent CAN message: " + str(message)) + ack_received = self.wait_for_ack( + expected_ack_id=message.id, timeout=1.0 + ) + if not ack_received: + attempts += 1 + self.debug_print( + f"ACK not received for message {i}. Attempt {attempts}" + ) + except Exception as e: + self.debug_print( + "Error Sending data over CAN bus" + + "".join(traceback.format_exception(None, e, e.__traceback__)) + ) + break + + if not ack_received: + self.debug_print( + f"Failed to receive ACK after {attempts} attempts for message {i}." + ) + return False + + return True + + def wait_for_ack(self, expected_ack_id, timeout): + """ + Waits for an ACK message with the specified ID within the given timeout period. + """ + start_time = time.monotonic() + while time.monotonic() - start_time < timeout: + ack_id = self.receive_ack_message() + + if ack_id is not None and ack_id == expected_ack_id: + return True + self.debug_print("No ACK received") + return False + + # =======================================================# + # ChatGPT Go Crazy # + # =======================================================# + + def listen_on_can_bus(self, process_message_callback, timeout=1.0): + """ + General purpose function to listen on the CAN bus and process messages using a callback function. + Add identical message rejection? + """ + with self.can_bus.listen(timeout=timeout) as listener: + message_count = listener.in_waiting() + for _ in range(message_count): + try: + msg = listener.receive() + result = process_message_callback(msg) + if result is not None: + return result + except Exception as e: + self.debug_print( + "Error processing message: " + + "".join(traceback.format_exception(None, e, e.__traceback__)) + ) + + def process_general_message(self, msg): + """ + Callback function for general message processing. + """ + # Send an ACK for the received message + self.send_ack(msg.id, is_extended=msg.extended) + + if isinstance(msg, RemoteTransmissionRequest): + return self.handle_remote_transmission_request(msg) + elif msg.id == self.MESSAGE_IDS["FAULT_ID"]: + return {"type": "FAULT", "content": msg.data} + elif msg.id == self.MESSAGE_IDS["SOT_ID"]: + self.handle_sot_message(msg) + elif msg.id == self.MESSAGE_IDS["EOT_ID"]: + self.handle_eot_message(msg) + elif msg.extended: + self.handle_multi_message(msg) + else: + self.handle_single_message(msg) + return None + + def send_ack(self, msg_id, is_extended=False): + """ + Sends an ACK message with the given message ID. + Args: + msg_id (int): The ID of the message to acknowledge. + is_extended (bool): True if the message ID is an extended ID, False otherwise. + """ + ack_data = b"ACK" # ACK message content + try: + ack_message = Message(id=msg_id, data=ack_data, extended=is_extended) + self.can_bus.send(ack_message) + self.debug_print(f"Sent ACK for message ID: {hex(msg_id)}") + except Exception as e: + self.debug_print( + "Error sending ACK: " + + "".join(traceback.format_exception(None, e, e.__traceback__)) + ) + + # =======================================================# + # Receive Handler Functions # + # =======================================================# + + def handle_remote_transmission_request(self, rtr): + """ + Handles a Remote Transmission Request and returns RTR info. + """ + # Implement handling of RTR + self.debug_print("RTR length: " + str(rtr.length)) + # Example: Return RTR ID + return {"type": "RTR", "id": rtr.id} + + def handle_multi_message(self, msg): + """ + Handles a part of a multi-message sequence. + """ + # Extract the original ID and the sequence number from the extended ID + original_id = msg.id >> 22 + sequence_number = msg.id & 0x3FFFFF # Mask to get the lower 22 bits + + self.debug_print( + f"Received multi-message chunk for ID: {original_id} with sequence number: {sequence_number}" + ) + + if ( + str(original_id) in self.multi_message_buffer + and not self.multi_message_buffer[str(original_id)]["is_complete"] + ): + # Store this chunk in the buffer + self.multi_message_buffer[str(original_id)]["received_chunks"][ + sequence_number + ] = msg.data + + # Check if all parts of the message have been received + # This can be done based on your protocol's specifics + if self.check_if_complete(original_id): + complete_message = self.reassemble_message(original_id) + # Process the complete message + self.process_complete_message(original_id, complete_message) + + else: + self.debug_print( + f"Unexpected multi-message chunk received for ID: {original_id}" + ) + + def check_if_complete(self, original_id): + """ + Checks if all parts of a multi-message sequence have been received. + """ + # Implement logic to determine if all parts are received + # This might involve checking sequence numbers, expected length, etc. + buffer = self.multi_message_buffer[str(original_id)] + return len(buffer["received_chunks"]) == buffer["expected_length"] + + def check_ack_message(self, msg): + return msg.id if msg.data == b"ACK" else None + + def reassemble_message(self, original_id): + """ + Reassembles all parts of a multi-message sequence into the complete message. + """ + buffer = self.multi_message_buffer[str(original_id)] + complete_message = b"".join( + buffer["received_chunks"][seq] for seq in sorted(buffer["received_chunks"]) + ) + buffer["is_complete"] = True + return complete_message + + def handle_single_message(self, msg): + """ + Handles a single message. Pretty much only used for debug. + """ + # Process a single, non-extended message + self.debug_print(f"Received single message with ID: {msg.id}") + self.debug_print(f"Message data: {msg.data}") + + def process_complete_message(self, original_id, message): + """ + Processes the complete reassembled message. + """ + # Implement your logic to handle the complete message + self.debug_print(f"Received complete message for ID: {original_id}") + self.debug_print(f"Message data: {message}") + + # =======================================================# + # Wrapper Functions # + # =======================================================# + + def receive_ack_message(self): + """ + Wrapper function to receive an ACK message. + """ + return self.listen_on_can_bus(self.check_ack_message, timeout=1.0) + + def listen_messages(self, timeout=1.0): + """ + Wrapper function to listen to general messages. + """ + return self.listen_on_can_bus(self.process_general_message, timeout) + + # =======================================================# + # Handshaking Functions # + # =======================================================# + + def send_sot(self, original_id, data_length): + """ + Sends a Start-of-Transmission message with the expected data length. + """ + sot_id = self.MESSAGE_IDS["SOT_ID"] + + # Combine the original_id and data_length into a single string, separated by a special character + data = f"{original_id}:{data_length}" + sot_message = Message(sot_id, data=bytes(data, "utf-8"), extended=False) + + try: + self.can_bus.send(sot_message) + self.debug_print( + f"Sent SOT for ID: {sot_id} with data length: {data_length}" + ) + except Exception as e: + self.debug_print( + "Error sending SOT: " + + "".join(traceback.format_exception(None, e, e.__traceback__)) + ) + + # Wait for ACK + return self.wait_for_ack(sot_id, 1.0) + + def handle_sot_message(self, msg): + """ + Processes the Start-of-Transmission (SOT) message. + """ + # Extract the data length from the message + try: + original_id, data_length = msg.data.decode("utf-8").split( + ":" + ) # Assuming data is sent as a string + data_length = int(data_length) + self.debug_print( + f"Received SOT for ID: {msg.id} with expected data length: {data_length}" + ) + except ValueError: + self.debug_print(f"Invalid data length format in SOT message: {msg.data}") + return + + # Send ACK for SOT message + self.send_ack(msg.id) + + # Initialize the buffer for the upcoming data stream + + if original_id not in self.multi_message_buffer: + self.multi_message_buffer[original_id] = { + "expected_length": data_length, + "received_chunks": {}, + "is_complete": False, + } + else: + # Reset the buffer if it already exists for this ID + self.multi_message_buffer[original_id]["expected_length"] = data_length + self.multi_message_buffer[original_id]["received_chunks"].clear() + self.multi_message_buffer[original_id]["is_complete"] = False + + self.debug_print(f"Initialized buffer for multi-message ID: {original_id}") + + def send_eot(self): + """ + Sends an End-of-Transmission message. + """ + eot_id = self.MESSAGE_IDS["EOT_ID"] + + eot_message = Message(eot_id, data=b"EOT", extended=False) + try: + self.can_bus.send(eot_message) + self.debug_print(f"Sent EOT for ID: {eot_id}") + except Exception as e: + self.debug_print( + "Error sending EOT: " + + "".join(traceback.format_exception(None, e, e.__traceback__)) + ) + + def handle_eot_message(self, msg): + """ + Processes the End-of-Transmission message. + """ + original_id = msg.id # Assuming the original ID is used in the EOT message + + # Validate the EOT message + + # Perform any cleanup or final processing + + # Send ACK for the EOT message + self.send_ack(msg.id) + + self.debug_print(f"Processed EOT for ID: {original_id}") + + # =======================================================# + # File Transfer Functions # + # =======================================================# + + def send_rtr_and_receive(self, rtr_id, timeout=5.0): + """ + Sends an RTR and waits for a response, which could be either single or multi-message. + """ + # Send RTR + rtr_message = RemoteTransmissionRequest(id=rtr_id) + try: + self.can_bus.send(rtr_message) + self.debug_print(f"Sent RTR with ID: {hex(rtr_id)}") + except Exception as e: + self.debug_print( + "Error sending RTR: " + + "".join(traceback.format_exception(None, e, e.__traceback__)) + ) + return None + + # Listen for responses + start_time = time.monotonic() + while time.monotonic() - start_time < timeout: + response = self.receive_response() + if response: + # Process response + if response["type"] == "SINGLE": + return response["data"] # Return single message data + elif response["type"] == "MULTI": + # Handle multi-message sequence + # You can either wait for the full sequence here or return and handle it elsewhere + pass + + return None + + def receive_response(self): + """ + Listens for a single message or the start of a multi-message sequence. + """ + msg = self.can_bus.receive() + if msg: + if msg.extended: + # Start of a multi-message sequence + self.handle_multi_message(msg) + return {"type": "MULTI"} + else: + # Single message response + return {"type": "SINGLE", "data": msg.data} + return None + + def request_file(self, file_id, timeout=5.0): + # Code from request_file goes here + rtr = RemoteTransmissionRequest(id=file_id) + self.can_bus.send(rtr) + + file_data = bytearray() + start_time = time.monotonic() + while True: + if time.monotonic() - start_time > timeout: + raise TimeoutError("No response received for file request") + msg = self.can_bus.receive() + if msg is None: + continue + if isinstance(msg, Message) and msg.id == file_id: + if msg.data == b"start": + continue + elif msg.data == b"stop": + break + else: + file_data.extend(msg.data) + return file_data diff --git a/FC_Board/lib/functions.py b/FC_Board/lib/functions.py index 1bb1de8c..75f30055 100755 --- a/FC_Board/lib/functions.py +++ b/FC_Board/lib/functions.py @@ -11,6 +11,9 @@ import traceback import random from debugcolor import co +from battery_helper import BatteryHelper +from packet_manager import PacketManager +from packet_sender import PacketSender class functions: @@ -21,15 +24,55 @@ def debug_print(self, statement): def __init__(self, cubesat): self.cubesat = cubesat + self.battery = BatteryHelper(cubesat) self.debug = cubesat.debug self.debug_print("Initializing Functionalities") + + self.pm = PacketManager(max_packet_size=128) + self.ps = PacketSender(cubesat.radio1, self.pm, max_retries=3) + self.Errorcount = 0 - self.facestring = [] + self.facestring = [None, None, None, None, None] self.jokes = [ - "Hey Its pretty cold up here, did someone forget to pay the electric bill?" + "Hey it is pretty cold up here, did someone forget to pay the electric bill?", + "sudo rf - rf*", + "Why did the astronaut break up with his girlfriend? He needed space.", + "Why did the sun go to school? To get a little brighter.", + "why is the mall called the mall? because instead of going to one store you go to them all", + "Alien detected. Blurring photo...", + "Wait it is all open source? Always has been... www.github.com/proveskit", + "What did 0 say to 1? You're a bit too much.", + "Pleiades - Orpheus has been recently acquired by the Onion News Network", + "This jokesat was brought to you by the Bronco Space Ministry of Labor and Job Placement", + "Catch you on the next pass!", + "Pleiades - Orpheus was not The Impostor", + "Sorry for messing with your long-exposure astrophoto!", + "Better buy a telescope. Wanna see me. Buy a telescope. Gonna be in space.", + "According to all known laws of aviation, there is no way bees should be able to fly...", + "You lost the game ", + "Bobby Tables is a good friend of mine", + "Why did the computer cross the road? To get a byte to eat!", + "Why are the astronauts not hungry when they got to space? They had a big launch.", + "Why did the computer get glasses? To improve its web sight!", + "What are computers favorite snacks? Chips!", + "Wait! I think I see a White 2019 Subaru Crosstrek 2.0i Premium", + "IS THAT A SUPRA?!", + "Finally escpaed the LA Traffic", + "My CubeSat is really good at jokes, but its delivery is always delayed.", + "exec order 66", + "I had a joke about UDP, but I am not sure if you'd get it.", + "I am not saying FSK modulation is the best way to send jokes, but at least it is never monotone!", + "I am sorry David, I am afrain I can not do that.", + "My memory is volatile like RAM, so it only makes sense that I forget things.", + "Imagine it gets stuck and just keeps repeating this joke every 2 mins", + "Check Engine: Error Code 404: Joke Not Found", + "CQ CQ KN6NAQ ... KN6NAT are you out there?", + "Woah is that the Launcher Orbiter?????", + "Everything in life is a spring if you think hard enough!", ] self.last_battery_temp = 20 - self.callsign = "Callsign" + self.sleep_duration = 30 + self.callsign = "KO6AZM" self.state_bool = False self.face_data_baton = False self.detumble_enable_z = True @@ -43,6 +86,35 @@ def __init__(self, cubesat): def current_check(self): return self.cubesat.current_draw + def safe_sleep(self, duration=15): + self.debug_print("Setting Safe Sleep Mode") + + self.cubesat.can_bus.sleep() + + iterations = 0 + + while duration > 15 and iterations < 12: + + time_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 listen_loiter(self): + self.debug_print("Listening for 10 seconds") + self.cubesat.watchdog_pet() + self.cubesat.radio1.receive_timeout = 10 + self.listen() + self.cubesat.watchdog_pet() + + self.debug_print("Sleeping for 20 seconds") + self.cubesat.watchdog_pet() + self.safe_sleep(self.sleep_duration) + self.cubesat.watchdog_pet() + """ Radio Functions """ @@ -58,8 +130,6 @@ def send(self, msg): self.field = Field.Field(self.cubesat, self.debug) message = f"{self.callsign} " + str(msg) + f" {self.callsign}" self.field.Beacon(message) - if self.cubesat.f_fsk: - self.cubesat.radio1.cw(message) if self.cubesat.is_licensed: self.debug_print(f"Sent Packet: " + message) else: @@ -67,17 +137,25 @@ def send(self, msg): del self.field del Field + def send_packets(self, data): + """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.ps.send_data(data) + def beacon(self): """Calls the RFM9x to send a beacon.""" import Field try: lora_beacon = ( - f"{self.callsign} Hello I am Yearling^2! I am in: " + f"{self.callsign} Hello I am Orpheus! I am: " + str(self.cubesat.power_mode) - + " power mode. V_Batt = " - + str(self.cubesat.battery_voltage) - + f"V. IHBPFJASTMNE! {self.callsign}" + + f" UT:{self.cubesat.uptime} BN:{self.cubesat.c_boot} EC:{self.cubesat.c_error_count} " + + f"IHBPFJASTMNE! {self.callsign}" ) except Exception as e: self.debug_print( @@ -94,14 +172,26 @@ def beacon(self): self.field = Field.Field(self.cubesat, self.debug) self.field.Beacon(lora_beacon) - if self.cubesat.f_fsk: - self.cubesat.radio1.cw(lora_beacon) del self.field del Field def joke(self): self.send(random.choice(self.jokes)) + def format_state_of_health(self, hardware): + to_return = "" + 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): import Field @@ -113,13 +203,13 @@ def state_of_health(self): f"VB:{self.cubesat.battery_voltage}", f"ID:{self.cubesat.current_draw}", f"IC:{self.cubesat.charge_current}", - f"VS:{self.cubesat.system_voltage}", f"UT:{self.cubesat.uptime}", f"BN:{self.cubesat.c_boot}", f"MT:{self.cubesat.micro.cpu.temperature}", f"RT:{self.cubesat.radio1.former_temperature}", f"AT:{self.cubesat.internal_temperature}", f"BT:{self.last_battery_temp}", + f"EC:{self.cubesat.c_error_count}", f"AB:{int(self.cubesat.burned)}", f"BO:{int(self.cubesat.f_brownout)}", f"FK:{int(self.cubesat.f_fsk)}", @@ -137,25 +227,13 @@ def state_of_health(self): + str(self.state_list) + f"{self.callsign}" ) - if self.cubesat.f_fsk: - self.cubesat.radio1.cw( - f"{self.callsign} Yearling^2 State of Health 1/2" - + str(self.state_list) - + f"{self.callsign}" - ) self.state_bool = True else: self.field.Beacon( f"{self.callsign} YSOH 2/2" - + str(self.cubesat.hardware) + + self.format_state_of_health(self.cubesat.hardware) + f"{self.callsign}" ) - if self.cubesat.f_fsk: - self.cubesat.radio1.cw( - f"{self.callsign} YSOH 2/2" - + str(self.cubesat.hardware) - + f"{self.callsign}" - ) self.state_bool = False del self.field del Field @@ -169,10 +247,6 @@ def send_face(self): self.field.Beacon( 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}" ) - if self.cubesat.f_fsk: - self.cubesat.radio1.cw( - 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}" - ) del self.field del Field @@ -183,7 +257,7 @@ def listen(self): try: self.debug_print("Listening") self.cubesat.radio1.receive_timeout = 10 - received = self.cubesat.radio1.receive(keep_listening=True) + received = self.cubesat.radio1.receive_with_ack(keep_listening=True) except Exception as e: self.debug_print( "An Error has occured while listening: " @@ -231,21 +305,46 @@ def listen_joke(self): def all_face_data(self): # self.cubesat.all_faces_on() + self.debug_print(gc.mem_free()) + gc.collect() + try: - print("New Function Needed!") + import Big_Data + + self.debug_print(gc.mem_free()) + + gc.collect() + a = Big_Data.AllFaces(self.debug, self.cubesat.tca) + self.debug_print(gc.mem_free()) + + self.facestring = a.Face_Test_All() + + del a + del Big_Data except Exception as e: self.debug_print("Big_Data error" + "".join(traceback.format_exception(e))) return self.facestring + def get_battery_data(self): + + try: + return self.battery.get_power_metrics() + + except Exception as e: + self.debug_print( + "Error retrieving battery data" + "".join(traceback.format_exception(e)) + ) + return None + def get_imu_data(self): try: data = [] - data.append(self.cubesat.IMU.Acceleration) - data.append(self.cubesat.IMU.Gyroscope) - data.append(self.cubesat.IMU.Magnetometer) + data.append(self.cubesat.accel) + data.append(self.cubesat.gyro) + data.append(self.cubesat.mag) except Exception as e: self.debug_print( "Error retrieving IMU data" + "".join(traceback.format_exception(e)) @@ -255,6 +354,7 @@ def get_imu_data(self): def OTA(self): # resets file system to whatever new file is received + self.debug_print("Implement an OTA Function Here") pass """ @@ -266,22 +366,16 @@ def log_face_data(self, data): self.debug_print("Logging Face Data") try: self.cubesat.log("/faces.txt", data) - except: - try: - self.cubesat.new_file("/faces.txt") - except Exception as e: - self.debug_print("SD error: " + "".join(traceback.format_exception(e))) + except Exception as e: + self.debug_print("SD error: " + "".join(traceback.format_exception(e))) def log_error_data(self, data): self.debug_print("Logging Error Data") try: self.cubesat.log("/error.txt", data) - except: - try: - self.cubesat.new_file("/error.txt") - except Exception as e: - self.debug_print("SD error: " + "".join(traceback.format_exception(e))) + except Exception as e: + self.debug_print("SD error: " + "".join(traceback.format_exception(e))) """ Misc Functions @@ -292,7 +386,7 @@ def log_error_data(self, data): def detumble(self, dur=7, margin=0.2, seq=118): self.debug_print("Detumbling") self.cubesat.RGB = (255, 255, 255) - self.cubesat.all_faces_on() + try: import Big_Data @@ -359,7 +453,7 @@ def Short_Hybernate(self): self.cubesat.enable_rf.value = False self.cubesat.f_softboot = True - time.sleep(120) + self.safe_sleep(120) self.cubesat.enable_rf.value = True return True @@ -371,7 +465,7 @@ def Long_Hybernate(self): self.cubesat.enable_rf.value = False self.cubesat.f_softboot = True - time.sleep(600) + self.safe_sleep(600) self.cubesat.enable_rf.value = True return True diff --git a/FC_Board/lib/packet_manager.py b/FC_Board/lib/packet_manager.py new file mode 100644 index 00000000..ae1787f3 --- /dev/null +++ b/FC_Board/lib/packet_manager.py @@ -0,0 +1,117 @@ +# Written with Claude 3.5 +# Nov 10, 2024 + + +class PacketManager: + def __init__(self, max_packet_size=128): + """Initialize the packet manager with maximum packet size (default 128 bytes for typical LoRa)""" + self.max_packet_size = max_packet_size + self.header_size = 4 # 2 bytes for sequence number, 2 for total packets + self.payload_size = max_packet_size - self.header_size + + def create_retransmit_request(self, missing_packets): + """ + 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 = b"\xFF\xFF" + len(missing_packets).to_bytes(2, "big") + payload = b"".join(seq.to_bytes(2, "big") for seq in missing_packets) + return header + payload + + def is_retransmit_request(self, packet): + """Check if packet is a retransmit request""" + return len(packet) >= 4 and packet[:2] == b"\xFF\xFF" + + def parse_retransmit_request(self, packet): + """Extract missing packet numbers from retransmit request""" + num_missing = int.from_bytes(packet[2:4], "big") + missing = [] + for i in range(num_missing): + start_idx = 4 + (i * 2) + seq = int.from_bytes(packet[start_idx : start_idx + 2], "big") + missing.append(seq) + return missing + + def pack_data(self, data): + """ + 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 = data.encode("utf-8") + else: + data = str(data).encode("utf-8") + + # Calculate number of packets needed + total_packets = (len(data) + self.payload_size - 1) // self.payload_size + print(f"Packing data of length {len(data)} into {total_packets} packets") + + packets = [] + for seq in range(total_packets): + # Create header + header = seq.to_bytes(2, "big") + total_packets.to_bytes(2, "big") + print(f"Created header: {[hex(b) for b in header]}") + + # Get payload slice for this packet + start = seq * self.payload_size + end = start + self.payload_size + payload = data[start:end] + + # Combine header and payload + packet = header + payload + print( + f"Packet {seq}: length={len(packet)}, header={[hex(b) for b in header]}" + ) + packets.append(packet) + + return packets + + def unpack_data(self, packets): + """ + 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 = sorted(packets, key=lambda p: int.from_bytes(p[:2], "big")) + except: + return None + + # Verify all packets are present + total_packets = 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 = b"".join(packet[self.header_size :] for packet in packets) + return data + + def create_ack_packet(self, seq_num): + """Creates an acknowledgment packet for a given sequence number""" + return b"ACK" + seq_num.to_bytes(2, "big") + + def is_ack_packet(self, packet): + """Checks if a packet is an acknowledgment packet""" + return packet.startswith(b"ACK") + + def get_ack_seq_num(self, ack_packet): + """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/FC_Board/lib/packet_sender.py b/FC_Board/lib/packet_sender.py new file mode 100644 index 00000000..fe35f89a --- /dev/null +++ b/FC_Board/lib/packet_sender.py @@ -0,0 +1,165 @@ +class PacketSender: + def __init__( + self, radio, packet_manager, ack_timeout=2.0, max_retries=3, send_delay=0.2 + ): + """ + Initialize the packet sender with optimized timing + """ + self.radio = radio + self.pm = packet_manager + self.ack_timeout = ack_timeout + self.max_retries = max_retries + self.send_delay = send_delay + + def wait_for_ack(self, expected_seq): + """ + Optimized ACK wait with early return + """ + import time + + start_time = time.monotonic() + + # Minimal delay after sending + time.sleep(self.send_delay) + + while (time.monotonic() - start_time) < self.ack_timeout: + packet = self.radio.receive() + + if packet and self.pm.is_ack_packet(packet): + ack_seq = self.pm.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, seq_num): + """Optimized packet sending with minimal delays""" + import time + + for attempt in range(self.max_retries): + self.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, progress_interval=10): + """Send data with minimal progress updates""" + packets = self.pm.pack_data(data) + total_packets = len(packets) + print(f"Sending {total_packets} packets...") + + for i, packet in enumerate(packets): + if i % progress_interval == 0: + print(f"Progress: {i}/{total_packets}") + + if not self.send_packet_with_retry(packet, i): + print(f"Failed at packet {i}/{total_packets}") + return False + + print(f"Successfully sent {total_packets} packets!") + return True + + def handle_retransmit_request(self, packets, request_packet): + """Handle retransmit request by sending requested packets""" + import time + + try: + missing_packets = self.pm.parse_retransmit_request(request_packet) + print(f"\nRetransmit request received for {len(missing_packets)} packets") + time.sleep(0.2) # Small delay before retransmission + + for seq in missing_packets: + if seq < len(packets): + print(f"Retransmitting packet {seq}") + self.radio.send(packets[seq]) + time.sleep(0.2) # Small delay between retransmitted packets + self.radio.send(packets[seq]) + time.sleep(0.2) # Small delay between retransmitted packets + + return True + + except Exception as e: + print(f"Error handling retransmit request: {e}") + return False + + def fast_send_data(self, data, send_delay=0.5, retransmit_wait=15.0): + """Send data with improved retransmission handling""" + import time + + packets = self.pm.pack_data(data) + total_packets = len(packets) + print(f"Sending {total_packets} packets...") + + # Send first packet with retry until ACKed + for attempt in range(self.max_retries): + print(f"Sending first packet (attempt {attempt + 1}/{self.max_retries})") + self.radio.send(packets[0]) + + if self.wait_for_ack(0): + break + else: + if attempt < self.max_retries - 1: + time.sleep(1.0) + else: + print("Failed to get ACK for first packet") + return False + + # Send remaining packets without waiting for ACKs + print("Sending remaining packets...") + for i in range(1, total_packets): + if i % 10 == 0: + print(f"Sending packet {i}/{total_packets}") + self.radio.send(packets[i]) + time.sleep(send_delay) + + print("\nWaiting for retransmit requests...") + retransmit_end_time = time.monotonic() + retransmit_wait + + while time.monotonic() < retransmit_end_time: + packet = self.radio.receive() + if packet: + print( + f"Received potential retransmit request: {[hex(b) for b in packet]}" + ) + + if self.pm.is_retransmit_request(packet): + print("Valid retransmit request received!") + missing_packets = self.pm.parse_retransmit_request(packet) + print(f"Retransmitting packets: {missing_packets}") + + # Add delay before retransmission to let receiver get ready + time.sleep(1) + + for seq in missing_packets: + if seq < len(packets): + print(f"Retransmitting packet {seq}") + self.radio.send(packets[seq]) + time.sleep( + 0.5 + ) # Longer delay between retransmitted packets + print(f"Retransmitting packet {seq}") + self.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 = time.monotonic() + retransmit_wait + + time.sleep(0.1) + + print("Finished sending all packets") + return True diff --git a/FC_Board/lib/pysquared.py b/FC_Board/lib/pysquared.py index e7998baf..894dad42 100755 --- a/FC_Board/lib/pysquared.py +++ b/FC_Board/lib/pysquared.py @@ -1,7 +1,7 @@ """ CircuitPython driver for PySquared satellite board. -PySquared Hardware Version: mainboard-v01 -CircuitPython Version: 8.0.0 alpha +PySquared Hardware Version: Flight Controller V4c +CircuitPython Version: 9.0.0 Library Repo: * Author(s): Nicole Maggard, Michael Pham, and Rachel Sarmiento @@ -9,7 +9,7 @@ # Common CircuitPython Libs import gc -import board, microcontroller +import board, machine, microcontroller import busio, time, sys, traceback from storage import mount, umount, VfsFat import digitalio, sdcardio, pwmio @@ -20,17 +20,23 @@ from collections import OrderedDict # Hardware Specific Libs -import pysquared_rfm9x # Radio +from adafruit_rfm import rfm9x # Radio +from adafruit_rfm import rfm9xfsk # Radio import neopixel # RGB LED from adafruit_lsm6ds.lsm6dsox import LSM6DSOX # IMU import adafruit_lis2mdl # Magnetometer import adafruit_tca9548a # I2C Multiplexer +import rv3028 +import adafruit_ov5640 + +# CAN Bus Import +from adafruit_mcp2515 import MCP2515 as CAN # NVM register numbers _BOOTCNT = const(0) _VBUSRST = const(6) -_STATECNT = const(7) +_ERRORCNT = const(7) _TOUTS = const(9) _ICHRG = const(11) _DIST = const(13) @@ -47,7 +53,7 @@ class Satellite: # General NVM counters c_boot = multiBitFlag(register=_BOOTCNT, lowest_bit=0, num_bits=8) c_vbusrst = multiBitFlag(register=_VBUSRST, lowest_bit=0, num_bits=8) - c_state_err = multiBitFlag(register=_STATECNT, lowest_bit=0, num_bits=8) + c_error_count = multiBitFlag(register=_ERRORCNT, lowest_bit=0, num_bits=8) c_distance = multiBitFlag(register=_DIST, lowest_bit=0, num_bits=8) c_ichrg = multiBitFlag(register=_ICHRG, lowest_bit=0, num_bits=8) @@ -62,10 +68,14 @@ class Satellite: f_fsk = bitFlag(register=_FLAG, bit=7) def debug_print(self, statement): + """ + A method for printing debug statements. + """ if self.debug: print(co("[pysquared]" + str(statement), "green", "bold")) def error_print(self, statement): + self.c_error_count = (self.c_error_count + 1) & 0xFF # Limited to 255 errors if self.debug: print(co("[pysquared]" + str(statement), "red", "bold")) @@ -76,15 +86,8 @@ def __init__(self): self.debug = True # Define verbose output here. True or False self.legacy = False # Define if the board is used with legacy or not self.heating = False # Currently not used - self.is_licensed = False - - """ - Define the boot time and current time - """ - self.BOOTTIME = 1577836800 - self.debug_print(f"Boot time: {self.BOOTTIME}s") - self.CURRENTTIME = self.BOOTTIME - self.UPTIME = 0 + self.orpheus = True # Define if the board is used with Orpheus or not + self.is_licensed = True """ Define the normal power modes @@ -98,6 +101,8 @@ def __init__(self): self.vlowbatt = 6.0 self.battery_voltage = 3.3 # default value for testing REPLACE WITH REAL VALUE self.current_draw = 255 # default value for testing REPLACE WITH REAL VALUE + self.REBOOT_TIME = 3600 # 1 hour + self.turbo_clock = False """ Setting up data buffers @@ -105,10 +110,28 @@ def __init__(self): self.data_cache = {} self.filenumbers = {} self.image_packets = 0 - self.urate = 115200 + self.urate = 9600 + self.buffer = None + self.buffer_size = 1 self.send_buff = memoryview(SEND_BUFF) self.micro = microcontroller + self.battery_voltage = None + self.draw_current = None + self.charge_voltage = None + self.charge_current = None + self.is_charging = None + self.battery_percentage = None + + """ + Define the boot time and current time + """ + self.c_boot += 1 + self.BOOTTIME = 1577836800 + self.debug_print(f"Boot time: {self.BOOTTIME}s") + self.CURRENTTIME = self.BOOTTIME + self.UPTIME = 0 + self.radio_cfg = { "id": 0xFB, "gs": 0xFA, @@ -119,7 +142,6 @@ def __init__(self): "pwr": 23, "st": 80000, } - self.hardware = OrderedDict( [ ("I2C0", False), @@ -139,6 +161,8 @@ def __init__(self): ("Face2", False), ("Face3", False), ("Face4", False), + ("CAM", False), + ("RTC", False), ] ) @@ -148,19 +172,31 @@ def __init__(self): if self.c_boot > 200: self.c_boot = 0 - if self.f_fsk: - self.debug_print("Fsk going to false") - self.f_fsk = False - if self.f_softboot: self.f_softboot = False + """ + Setting up the watchdog pin. + """ + + self.watchdog_pin = digitalio.DigitalInOut(board.WDT_WDI) + self.watchdog_pin.direction = digitalio.Direction.OUTPUT + self.watchdog_pin.value = False + + """ + Set the CPU Clock Speed + """ + machine.set_clock(62500000) + """ Intializing Communication Buses """ try: - self.i2c0 = busio.I2C(board.I2C0_SCL, board.I2C0_SDA) - self.hardware["I2C0"] = True + if not self.orpheus: + self.i2c0 = busio.I2C(board.I2C0_SCL, board.I2C0_SDA) + self.hardware["I2C0"] = True + else: + self.debug_print("[Orpheus] I2C0 not initialized") except Exception as e: self.error_print( @@ -186,8 +222,15 @@ def __init__(self): ) try: - self.uart = busio.UART(board.TX, board.RX, baudrate=self.urate) - self.hardware["UART"] = True + if not self.orpheus: + self.uart = busio.UART(board.TX, board.RX, baudrate=self.urate) + self.hardware["UART"] = True + else: + # Orpheus uses the I2C0 Connection for UART + self.uart = busio.UART( + board.I2C0_SDA, board.I2C0_SCL, baudrate=self.urate + ) + self.hardware["UART"] = True except Exception as e: self.error_print( @@ -222,31 +265,41 @@ def __init__(self): self.radio1_DIO4.switch_to_input() try: - self.radio1 = pysquared_rfm9x.RFM9x( - self.spi0, - _rf_cs1, - _rf_rst1, - self.radio_cfg["freq"], - code_rate=8, - baudrate=1320000, - ) - # Default LoRa Modulation Settings - # Frequency: 437.4 MHz, SF7, BW125kHz, CR4/8, Preamble=8, CRC=True - self.radio1.dio0 = self.radio1_DIO0 - # self.radio1.dio4=self.radio1_DIO4 - self.radio1.max_output = True - self.radio1.tx_power = self.radio_cfg["pwr"] - self.radio1.spreading_factor = self.radio_cfg["sf"] + if self.f_fsk: + self.radio1 = rfm9xfsk.RFM9xFSK( + self.spi0, + _rf_cs1, + _rf_rst1, + self.radio_cfg["freq"], + # code_rate=8, code rate does not exist for RFM9xFSK + ) + self.radio1.fsk_node_address = 1 + self.radio1.fsk_broadcast_address = 0xFF + self.radio1.modulation_type = 0 + else: + # Default LoRa Modulation Settings + # Frequency: 437.4 MHz, SF7, BW125kHz, CR4/8, Preamble=8, CRC=True + self.radio1 = rfm9x.RFM9x( + self.spi0, + _rf_cs1, + _rf_rst1, + self.radio_cfg["freq"], + # code_rate=8, code rate does not exist for RFM9xFSK + ) + self.radio1.max_output = True + self.radio1.tx_power = self.radio_cfg["pwr"] + self.radio1.spreading_factor = self.radio_cfg["sf"] + + self.radio1.enable_crc = True + self.radio1.ack_delay = 0.2 + if self.radio1.spreading_factor > 9: + self.radio1.preamble_length = self.radio1.spreading_factor self.radio1.node = self.radio_cfg["id"] self.radio1.destination = self.radio_cfg["gs"] - self.radio1.enable_crc = True - self.radio1.ack_delay = 0.2 - if self.radio1.spreading_factor > 9: - self.radio1.preamble_length = self.radio1.spreading_factor self.hardware["Radio1"] = True - if self.legacy: - self.enable_rf.value = False + # if self.legacy: + # self.enable_rf.value = False except Exception as e: self.error_print( @@ -270,6 +323,36 @@ def __init__(self): self.error_print("[ERROR][Magnetometer]") traceback.print_exception(None, e, e.__traceback__) + """ + CAN Transceiver Initialization + """ + try: + self.spi0cs2 = digitalio.DigitalInOut(board.SPI0_CS2) + self.spi0cs2.switch_to_output() + self.can_bus = CAN(self.spi0, self.spi0cs2, loopback=True, silent=True) + self.hardware["CAN"] = True + self.can_bus.sleep() + + except Exception as e: + self.debug_print( + "[ERROR][CAN TRANSCEIVER]" + "".join(traceback.format_exception(e)) + ) + + """ + RTC Initialization + """ + try: + self.rtc = rv3028.RV3028(self.i2c1) + + # Still need to test these configs + self.rtc.configure_backup_switchover(mode="level", interrupt=True) + self.hardware["RTC"] = True + + except Exception as e: + self.debug_print( + "[ERROR][Real Time Clock]" + "".join(traceback.format_exception(e)) + ) + """ SD Card Initialization """ @@ -308,15 +391,73 @@ def __init__(self): try: self.tca = adafruit_tca9548a.TCA9548A(self.i2c1, address=int(0x77)) self.hardware["TCA"] = True + except OSError: + self.error_print( + "[ERROR][TCA] TCA try_lock failed. TCA may be malfunctioning." + ) + self.hardware["TCA"] = False + return except Exception as e: self.error_print("[ERROR][TCA]" + "".join(traceback.format_exception(e))) """ Face Initializations """ - self.scan_tca_channels() + """ + Camera Initialization + """ + if self.hardware["TCA"] is True: + try: + self.cam = adafruit_ov5640.OV5640( + self.tca[5], + data_pins=( + board.D2, + board.D3, + board.D4, + board.D5, + board.D6, + board.D7, + board.D8, + board.D9, + ), + clock=board.PC, + vsync=board.VS, + href=board.HS, + mclk=None, + shutdown=None, + reset=None, + size=adafruit_ov5640.OV5640_SIZE_QVGA, + ) + + self.cam.colorspace = adafruit_ov5640.OV5640_COLOR_JPEG + self.cam.flip_y = False + self.cam.flip_x = False + self.cam.test_pattern = False + + self.cam.effect = 0 + self.cam.exposure_value = -2 + self.cam.white_balance = 2 + self.cam.night_mode = False + self.cam.quality = 20 + + self.hardware["CAM"] = True + + except Exception as e: + self.error_print( + "[ERROR][CAMERA]" + "".join(traceback.format_exception(e)) + ) + self.hardware["CAM"] = False + + else: + self.error_print("[ERROR][CAMERA]TCA Not Initialized") + self.hardware["CAM"] = False + + if self.f_fsk: + self.debug_print("Next restart will be in LoRa mode.") + self.f_fsk = False + """ Prints init State of PySquared Hardware """ @@ -386,6 +527,8 @@ def _scan_single_channel(self, channel, channel_to_face): self.debug_print([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.error_print(f"[ERROR][FACE]{traceback.format_exception(e)}") finally: self.tca[channel].unlock() @@ -393,6 +536,23 @@ def _scan_single_channel(self, channel, channel_to_face): Code to call satellite parameters """ + @property + def turbo(self): + return self.turbo_clock + + @turbo.setter + def turbo(self, value): + self.turbo_clock = value + + try: + if value is True: + machine.set_clock(125000000) # 125Mhz + else: + machine.set_clock(62500000) # 62.5Mhz + + except Exception as e: + self.error_print(f"[ERROR][CLOCK SPEED]{traceback.format_exception(e)}") + @property def burnarm(self): return self.f_burnarm @@ -434,15 +594,13 @@ def reset_vbus(self): if self.hardware["SDcard"]: try: umount("/sd") - self.spi.deinit() time.sleep(3) except Exception as e: self.error_print( "error unmounting SD card" + "".join(traceback.format_exception(e)) ) try: - self._resetReg.drive_mode = digitalio.DriveMode.PUSH_PULL - self._resetReg.value = 1 + self.debug_print("Resetting VBUS [IMPLEMENT NEW FUNCTION HERE]") except Exception as e: self.error_print( "vbus reset error: " + "".join(traceback.format_exception(e)) @@ -463,7 +621,7 @@ def accel(self): self.error_print("[ERROR][ACCEL]" + "".join(traceback.format_exception(e))) @property - def imu_temp(self): + def internal_temperature(self): try: return self.imu.temperature except Exception as e: @@ -476,6 +634,119 @@ def mag(self): except Exception as e: self.error_print("[ERROR][mag]" + "".join(traceback.format_exception(e))) + @property + def time(self): + try: + return self.rtc.get_time() + except Exception as e: + self.error_print("[ERROR][RTC]" + "".join(traceback.format_exception(e))) + + @time.setter + def time(self, hours, minutes, seconds): + if self.hardware["RTC"]: + try: + self.rtc.set_time(hours, minutes, seconds) + except Exception as e: + self.error_print( + "[ERROR][RTC]" + "".join(traceback.format_exception(e)) + ) + else: + self.error_print("[WARNING] RTC not initialized") + + @property + def date(self): + try: + return self.rtc.get_date() + except Exception as e: + self.error_print("[ERROR][RTC]" + "".join(traceback.format_exception(e))) + + @date.setter + def date(self, year, month, date, weekday): + if self.hardware["RTC"]: + try: + self.rtc.set_date(year, month, date, weekday) + except Exception as e: + self.error_print( + "[ERROR][RTC]" + "".join(traceback.format_exception(e)) + ) + else: + self.error_print("[WARNING] RTC not initialized") + + """ + Camera Functions + """ + + def take_image(self): + try: + gc.collect() + self.buffer_size = self.cam.height * self.cam.width // self.cam.quality + self.buffer = bytearray(self.buffer_size) + self.cam.capture(self.buffer) + + eoi = self.buffer.find(b"\xff\xd9") + if eoi != -1: + # terminate the JPEG data just after the EOI marker + print(memoryview(self.buffer)[: eoi + 2].hex()) + else: + print("image corrupted!") + print(memoryview(self.buffer).hex()) + + except Exception as e: + self.error_print("[ERROR][CAMERA]" + "".join(traceback.format_exception(e))) + + finally: + self.buffer = None + + """ + Maintenence Functions + """ + + def watchdog_pet(self): + self.watchdog_pin.value = True + time.sleep(0.01) + self.watchdog_pin.value = False + + def check_reboot(self): + self.UPTIME = self.uptime + self.debug_print(str("Current up time: " + str(self.UPTIME))) + if self.UPTIME > self.REBOOT_TIME: + self.micro.reset() + + def powermode(self, mode): + """ + 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 = "critical" + + elif "min" in mode: + self.neopixel.brightness = 0 + self.enable_rf.value = False + + self.power_mode = "minimum" + + elif "norm" in mode: + self.enable_rf.value = True + self.power_mode = "normal" + # don't forget to reconfigure radios, gps, etc... + + elif "max" in mode: + self.enable_rf.value = True + self.power_mode = "maximum" + except Exception as e: + self.error_print( + "Error in changing operations of powermode: " + + "".join(traceback.format_exception(e)) + ) + + """ + SD Card Functions + """ + def log(self, filedir, msg): if self.hardware["SDcard"]: try: @@ -490,12 +761,6 @@ def log(self, filedir, msg): else: self.error_print("[WARNING] SD Card not initialized") - def check_reboot(self): - self.UPTIME = self.uptime - self.debug_print(str("Current up time: " + str(self.UPTIME))) - if self.UPTIME > 86400: - self.micro.reset() - def print_file(self, filedir=None, binary=False): try: if filedir == None: @@ -534,37 +799,6 @@ def read_file(self, filedir=None, binary=False): "[ERROR] Cant print file: " + "".join(traceback.format_exception(e)) ) - def powermode(self, mode): - """ - 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 = "critical" - - elif "min" in mode: - self.neopixel.brightness = 0 - self.enable_rf.value = False - - self.power_mode = "minimum" - - elif "norm" in mode: - self.enable_rf.value = True - self.power_mode = "normal" - # don't forget to reconfigure radios, gps, etc... - - elif "max" in mode: - self.enable_rf.value = True - self.power_mode = "maximum" - except Exception as e: - self.error_print( - "Error in changing operations of powermode: " - + "".join(traceback.format_exception(e)) - ) - def new_file(self, substring, binary=False): """ substring something like '/data/DATA_' diff --git a/FC_Board/lib/pysquared_rfm9x.py b/FC_Board/lib/pysquared_rfm9x.py deleted file mode 100755 index 1f221ab6..00000000 --- a/FC_Board/lib/pysquared_rfm9x.py +++ /dev/null @@ -1,1116 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Tony DiCola for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -""" -MODIFIED VERSION of adafruit_rfm9x CircuitPython Library for PyCubed Use -See https://github.com/adafruit/Adafruit_CircuitPython_RFM9x - -CircuitPython Version: 7.0.0 alpha -Library Repo: https://github.com/pycubed/library_pycubed.py -* Edits by: Max Holliday -Added temperature readout by Nicole Maggard -""" -import time -from random import random -import digitalio -from micropython import const -import adafruit_bus_device.spi_device as spidev - -# pylint: disable=bad-whitespace -# Internal constants: -# Register names (FSK Mode even though we use LoRa instead, from table 85) -_RH_RF95_REG_00_FIFO = const(0x00) -_RH_RF95_REG_01_OP_MODE = const(0x01) -_RH_RF95_REG_06_FRF_MSB = const(0x06) -_RH_RF95_REG_07_FRF_MID = const(0x07) -_RH_RF95_REG_08_FRF_LSB = const(0x08) -_RH_RF95_REG_09_PA_CONFIG = const(0x09) -_RH_RF95_REG_0A_PA_RAMP = const(0x0A) -_RH_RF95_REG_0B_OCP = const(0x0B) -_RH_RF95_REG_0C_LNA = const(0x0C) -_RH_RF95_REG_0D_FIFO_ADDR_PTR = const(0x0D) -_RH_RF95_REG_0E_FIFO_TX_BASE_ADDR = const(0x0E) -_RH_RF95_REG_0F_FIFO_RX_BASE_ADDR = const(0x0F) -_RH_RF95_REG_10_FIFO_RX_CURRENT_ADDR = const(0x10) -_RH_RF95_REG_11_IRQ_FLAGS_MASK = const(0x11) -_RH_RF95_REG_12_IRQ_FLAGS = const(0x12) -_RH_RF95_REG_13_RX_NB_BYTES = const(0x13) -_RH_RF95_REG_14_RX_HEADER_CNT_VALUE_MSB = const(0x14) -_RH_RF95_REG_15_RX_HEADER_CNT_VALUE_LSB = const(0x15) -_RH_RF95_REG_16_RX_PACKET_CNT_VALUE_MSB = const(0x16) -_RH_RF95_REG_17_RX_PACKET_CNT_VALUE_LSB = const(0x17) -_RH_RF95_REG_18_MODEM_STAT = const(0x18) -_RH_RF95_REG_19_PKT_SNR_VALUE = const(0x19) -_RH_RF95_REG_1A_PKT_RSSI_VALUE = const(0x1A) -_RH_RF95_REG_1B_RSSI_VALUE = const(0x1B) -_RH_RF95_REG_1C_HOP_CHANNEL = const(0x1C) -_RH_RF95_REG_1D_MODEM_CONFIG1 = const(0x1D) -_RH_RF95_REG_1E_MODEM_CONFIG2 = const(0x1E) -_RH_RF95_REG_1F_SYMB_TIMEOUT_LSB = const(0x1F) -_RH_RF95_REG_20_PREAMBLE_MSB = const(0x20) -_RH_RF95_REG_21_PREAMBLE_LSB = const(0x21) -_RH_RF95_REG_22_PAYLOAD_LENGTH = const(0x22) -_RH_RF95_REG_23_MAX_PAYLOAD_LENGTH = const(0x23) -_RH_RF95_REG_24_HOP_PERIOD = const(0x24) -_RH_RF95_REG_25_FIFO_RX_BYTE_ADDR = const(0x25) -_RH_RF95_REG_26_MODEM_CONFIG3 = const(0x26) - -_RH_RF95_REG_3C_REGTEMP = const(0x3C) - -_RH_RF95_REG_40_DIO_MAPPING1 = const(0x40) -_RH_RF95_REG_41_DIO_MAPPING2 = const(0x41) -_RH_RF95_REG_42_VERSION = const(0x42) - -_RH_RF95_REG_4B_TCXO = const(0x4B) -_RH_RF95_REG_4D_PA_DAC = const(0x4D) -_RH_RF95_REG_5B_FORMER_TEMP = const(0x5B) -_RH_RF95_REG_61_AGC_REF = const(0x61) -_RH_RF95_REG_62_AGC_THRESH1 = const(0x62) -_RH_RF95_REG_63_AGC_THRESH2 = const(0x63) -_RH_RF95_REG_64_AGC_THRESH3 = const(0x64) - -_RH_RF95_DETECTION_OPTIMIZE = const(0x31) -_RH_RF95_DETECTION_THRESHOLD = const(0x37) - -_RH_RF95_PA_DAC_DISABLE = const(0x04) -_RH_RF95_PA_DAC_ENABLE = const(0x07) - -# The Frequency Synthesizer step = RH_RF95_FXOSC / 2^^19 -_RH_RF95_FSTEP = 32000000 / 524288 - -# RadioHead specific compatibility constants. -_RH_BROADCAST_ADDRESS = const(0xFF) - -# The acknowledgement bit in the FLAGS -# The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved -# for application layer use. -_RH_FLAGS_ACK = const(0x80) -_RH_FLAGS_RETRY = const(0x40) - -# User facing constants: -SLEEP_MODE = const(0) # 0b000 -STANDBY_MODE = const(1) # 0b001 -FS_TX_MODE = const(2) # 0b010 -TX_MODE = const(3) # 0b011 -FS_RX_MODE = const(4) # 0b100 -RX_MODE = const(5) # 0b101 -# pylint: enable=bad-whitespace - -# gap =bytes([0xFF]) -# sgap=bytes([0xFF,0xFF,0xFF]) -# dot =bytes([0]) -# dash=bytes([0,0,0]) -# # ...- .-. ...-- -..- -# VR3X = (gap+(dot+gap)*3)+dash+sgap+\ -# (dot+gap)+dash+gap+dot+sgap+\ -# ((dot+gap)*3)+dash+gap+dash+sgap+\ -# dash+gap+((dot+gap)*2)+dash+gap -VR3X = b"\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\xff\xff\xff\x00\xff\x00\x00\x00\xff\x00\xff\xff\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\x00\x00\x00\xff\x00\xff\x00\xff\x00\x00\x00\xff" - - -# Disable the too many instance members warning. Pylint has no knowledge -# of the context and is merely guessing at the proper amount of members. This -# is a complex chip which requires exposing many attributes and state. Disable -# the warning to work around the error. -# pylint: disable=too-many-instance-attributes - -_bigbuffer = bytearray(256) -bw_bins = (7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000) - - -class RFM9x: - """Interface to a RFM95/6/7/8 LoRa radio module. Allows sending and - receivng bytes of data in long range LoRa mode at a support board frequency - (433/915mhz). - - You must specify the following parameters: - - spi: The SPI bus connected to the radio. - - cs: The CS pin DigitalInOut connected to the radio. - - reset: The reset/RST pin DigialInOut connected to the radio. - - frequency: The frequency (in mhz) of the radio module (433/915mhz typically). - - You can optionally specify: - - preamble_length: The length in bytes of the packet preamble (default 8). - - high_power: Boolean to indicate a high power board (RFM95, etc.). Default - is True for high power. - - baudrate: Baud rate of the SPI connection, default is 10mhz but you might - choose to lower to 1mhz if using long wires or a breadboard. - - Remember this library makes a best effort at receiving packets with pure - Python code. Trying to receive packets too quickly will result in lost data - so limit yourself to simple scenarios of sending and receiving single - packets at a time. - - Also note this library tries to be compatible with raw RadioHead Arduino - library communication. This means the library sets up the radio modulation - to match RadioHead's defaults and assumes that each packet contains a - 4 byte header compatible with RadioHead's implementation. - Advanced RadioHead features like address/node specific packets - or "reliable datagram" delivery are supported however due to the - limitations noted, "reliable datagram" is still subject to missed packets but with it, - sender is notified if a packet has potentially been missed. - """ - - # Global buffer for SPI commands - _BUFFER = bytearray(4) - DEBUG_HEADER = False - valid_ids = (58, 59, 60, 255) - - class _RegisterBits: - # Class to simplify access to the many configuration bits avaialable - # on the chip's registers. This is a subclass here instead of using - # a higher level module to increase the efficiency of memory usage - # (all of the instances of this bit class will share the same buffer - # used by the parent RFM69 class instance vs. each having their own - # buffer and taking too much memory). - - # Quirk of pylint that it requires public methods for a class. This - # is a decorator class in Python and by design it has no public methods. - # Instead it uses dunder accessors like get and set below. For some - # reason pylint can't figure this out so disable the check. - # pylint: disable=too-few-public-methods - - # Again pylint fails to see the true intent of this code and warns - # against private access by calling the write and read functions below. - # This is by design as this is an internally used class. Disable the - # check from pylint. - # pylint: disable=protected-access - - def __init__(self, address, *, offset=0, bits=1): - assert 0 <= offset <= 7 - assert 1 <= bits <= 8 - assert (offset + bits) <= 8 - self._address = address - self._mask = 0 - for _ in range(bits): - self._mask <<= 1 - self._mask |= 1 - self._mask <<= offset - self._offset = offset - - def __get__(self, obj, objtype): - reg_value = obj._read_u8(self._address) - return (reg_value & self._mask) >> self._offset - - def __set__(self, obj, val): - reg_value = obj._read_u8(self._address) - reg_value &= ~self._mask - reg_value |= (val & 0xFF) << self._offset - obj._write_u8(self._address, reg_value) - - operation_mode = _RegisterBits(_RH_RF95_REG_01_OP_MODE, bits=3) - - low_frequency_mode = _RegisterBits(_RH_RF95_REG_01_OP_MODE, offset=3, bits=1) - - osc_calibration = _RegisterBits(_RH_RF95_REG_24_HOP_PERIOD, offset=3, bits=1) - - modulation_type = _RegisterBits(_RH_RF95_REG_01_OP_MODE, offset=5, bits=2) - - # Long range/LoRa mode can only be set in sleep mode! - long_range_mode = _RegisterBits(_RH_RF95_REG_01_OP_MODE, offset=7, bits=1) - - lna_boost = _RegisterBits(_RH_RF95_REG_0C_LNA, bits=2) - - output_power = _RegisterBits(_RH_RF95_REG_09_PA_CONFIG, bits=4) - - modulation_shaping = _RegisterBits(_RH_RF95_REG_0A_PA_RAMP, bits=2) - - pa_ramp = _RegisterBits(_RH_RF95_REG_0A_PA_RAMP, bits=4) - - max_power = _RegisterBits(_RH_RF95_REG_09_PA_CONFIG, offset=4, bits=3) - - pa_select = _RegisterBits(_RH_RF95_REG_09_PA_CONFIG, offset=7, bits=1) - - pa_dac = _RegisterBits(_RH_RF95_REG_4D_PA_DAC, bits=3) - - dio0_mapping = _RegisterBits(_RH_RF95_REG_40_DIO_MAPPING1, offset=6, bits=2) - - low_datarate_optimize = _RegisterBits( - _RH_RF95_REG_26_MODEM_CONFIG3, offset=3, bits=1 - ) - auto_agc = _RegisterBits(_RH_RF95_REG_26_MODEM_CONFIG3, offset=2, bits=1) - - debug = False - buffview = memoryview(_bigbuffer) - - def __init__( - self, - spi, - cs, - reset, - frequency, - *, - preamble_length=8, - code_rate=5, - high_power=True, - baudrate=5000000, - max_output=False - ): - self.high_power = high_power - self.max_output = max_output - self.dio0 = False - # Device support SPI mode 0 (polarity & phase = 0) up to a max of 10mhz. - # Set Default Baudrate to 5MHz to avoid problems - self._device = spidev.SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=0) - # Setup reset as a digital input (default state for reset line according - # to the datasheet). This line is pulled low as an output quickly to - # trigger a reset. Note that reset MUST be done like this and set as - # a high impedence input or else the chip cannot change modes (trust me!). - self._reset = reset - self._reset.switch_to_input(pull=digitalio.Pull.UP) - self.reset() - # No device type check! Catch an error from the very first request and - # throw a nicer message to indicate possible wiring problems. - version = self._read_u8(_RH_RF95_REG_42_VERSION) - if version != 18: - raise RuntimeError( - "Failed to find rfm9x with expected version -- check wiring" - ) - - # Set sleep mode, wait 10ms and confirm in sleep mode (basic device check). - # Also set long range mode (LoRa mode) as it can only be done in sleep. - self.idle() - time.sleep(0.01) - self.osc_calibration = True - time.sleep(1) - - self.sleep() - time.sleep(0.01) - self.long_range_mode = True - if self.operation_mode != SLEEP_MODE or not self.long_range_mode: - raise RuntimeError("Failed to configure radio for LoRa mode, check wiring!") - # clear default setting for access to LF registers if frequency > 525MHz - if frequency > 525: - self.low_frequency_mode = 0 - # Setup entire 256 byte FIFO - self._write_u8(_RH_RF95_REG_0E_FIFO_TX_BASE_ADDR, 0x00) - self._write_u8(_RH_RF95_REG_0F_FIFO_RX_BASE_ADDR, 0x00) - # Disable Freq Hop - self._write_u8(_RH_RF95_REG_24_HOP_PERIOD, 0x00) - # Set mode idle - self.idle() - - # Set frequency - self.frequency_mhz = frequency - # Set preamble length (default 8 bytes to match radiohead). - self.preamble_length = preamble_length - # Defaults set modem config to RadioHead compatible Bw125Cr45Sf128 mode. - self.signal_bandwidth = 125000 - self.coding_rate = code_rate - self.spreading_factor = 7 - # Default to disable CRC checking on incoming packets. - self.enable_crc = False - # Note no sync word is set for LoRa mode either! - self._write_u8(_RH_RF95_REG_26_MODEM_CONFIG3, 0x00) - # Set transmit power to 13 dBm, a safe value any module supports. - self.tx_power = 13 - # initialize last RSSI reading - self.last_rssi = 0.0 - """The RSSI of the last received packet. Stored when the packet was received. - This instantaneous RSSI value may not be accurate once the - operating mode has been changed. - """ - # initialize timeouts and delays delays - self.ack_wait = 0.5 - """The delay time before attempting a retry after not receiving an ACK""" - self.receive_timeout = 0.5 - """The amount of time to poll for a received packet. - If no packet is received, the returned packet will be None - """ - self.xmit_timeout = 2.0 - """The amount of time to wait for the HW to transmit the packet. - This is mainly used to prevent a hang due to a HW issue - """ - self.ack_retries = 5 - """The number of ACK retries before reporting a failure.""" - self.ack_delay = None - """The delay time before attemting to send an ACK. - If ACKs are being missed try setting this to .1 or .2. - """ - # initialize sequence number counter for reliabe datagram mode - self.sequence_number = 0 - # create seen Ids list - self.seen_ids = bytearray(256) - # initialize packet header - # node address - default is broadcast - self.node = _RH_BROADCAST_ADDRESS - """The default address of this Node. (0-255). - If not 255 (0xff) then only packets address to this node will be accepted. - First byte of the RadioHead header. - """ - # destination address - default is broadcast - self.destination = _RH_BROADCAST_ADDRESS - """The default destination address for packet transmissions. (0-255). - If 255 (0xff) then any receiving node should accept the packet. - Second byte of the RadioHead header. - """ - # ID - contains seq count for reliable datagram mode - self.identifier = 0 - """Automatically set to the sequence number when send_with_ack() used. - Third byte of the RadioHead header. - """ - # flags - identifies ack/reetry packet for reliable datagram mode - self.flags = 0 - """Upper 4 bits reserved for use by Reliable Datagram Mode. - Lower 4 bits may be used to pass information. - Fourth byte of the RadioHead header. - """ - self.crc_error_count = 0 - - self.auto_agc = True - self.pa_ramp = 0 # mode agnostic - self.lna_boost = 3 # mode agnostic - - def cw(self, msg=None): - success = False - if msg is None: - msg = VR3X - - cache = [] - if self.long_range_mode: - # cache LoRa params - cache = [ - self.spreading_factor, - self.signal_bandwidth, - self.coding_rate, - self.preamble_length, - self.enable_crc, - ] - - self.operation_mode = SLEEP_MODE - time.sleep(0.01) - self.long_range_mode = False # FSK/OOK Mode - self.modulation_type = 0 # FSK - self.modulation_shaping = 2 - self._write_u8(0x25, 0x00) # no preamble - self._write_u8(0x26, 0x00) # no preamble - self._write_u8(0x27, 0x00) # no sync word - self._write_u8(0x3F, 10) # clear FIFO - self._write_u8(0x02, 0xFF) # BitRate(15:8) - self._write_u8(0x03, 0xFF) # BitRate(15:8) - self._write_u8(0x05, 11) # Freq deviation Lsb 600 Hz - self.idle() - # Set payload length VR3X Morse length = 51 - self._write_u8(0x35, len(msg) - 1) - self._write_from(_RH_RF95_REG_00_FIFO, bytearray(msg)) - - _t = time.monotonic() + 10 - self.operation_mode = TX_MODE - while time.monotonic() < _t: - a = self._read_u8(0x3F) - # print(a,end=' ') - if (a >> 6) & 1: - time.sleep(0.01) - success = True - break - if not (a >> 6) & 1: - print("cw timeout") - self.idle() - if cache: - self.operation_mode = SLEEP_MODE - time.sleep(0.01) - self.long_range_mode = True - self._write_u8(_RH_RF95_REG_0E_FIFO_TX_BASE_ADDR, 0x00) - self._write_u8(_RH_RF95_REG_0F_FIFO_RX_BASE_ADDR, 0x00) - self._write_u8(_RH_RF95_REG_24_HOP_PERIOD, 0x00) - self.idle() - self.spreading_factor = cache[0] - self.signal_bandwidth = cache[1] - self.coding_rate = cache[2] - self.preamble_length = cache[3] - self.enable_crc = cache[4] - self.auto_agc = True - return success - - # pylint: disable=no-member - # Reconsider pylint: disable when this can be tested - def _read_into(self, address, buf, length=None): - # Read a number of bytes from the specified address into the provided - # buffer. If length is not specified (the default) the entire buffer - # will be filled. - if length is None: - length = len(buf) - with self._device as device: - self._BUFFER[0] = address & 0x7F # Strip out top bit to set 0 - # value (read). - device.write(self._BUFFER, end=1) - device.readinto(buf, end=length) - - def _read_u8(self, address): - # Read a single byte from the provided address and return it. - self._read_into(address, self._BUFFER, length=1) - return self._BUFFER[0] - - def _write_from(self, address, buf, length=None): - # Write a number of bytes to the provided address and taken from the - # provided buffer. If no length is specified (the default) the entire - # buffer is written. - if length is None: - length = len(buf) - with self._device as device: - self._BUFFER[0] = (address | 0x80) & 0xFF # Set top bit to 1 to - # indicate a write. - device.write(self._BUFFER, end=1) - device.write(buf, end=length) - - def _write_u8(self, address, val): - # Write a byte register to the chip. Specify the 7-bit address and the - # 8-bit value to write to that address. - with self._device as device: - self._BUFFER[0] = (address | 0x80) & 0xFF # Set top bit to 1 to - # indicate a write. - self._BUFFER[1] = val & 0xFF - device.write(self._BUFFER, end=2) - - def reset(self): - """Perform a reset of the chip.""" - # See section 7.2.2 of the datasheet for reset description. - self._reset.switch_to_output(value=False) - time.sleep(0.0001) # 100 us - self._reset.switch_to_input(pull=digitalio.Pull.UP) - time.sleep(0.005) # 5 ms - - def idle(self): - """Enter idle standby mode.""" - self.operation_mode = STANDBY_MODE - - def sleep(self): - """Enter sleep mode.""" - self.operation_mode = SLEEP_MODE - - def listen(self): - """Listen for packets to be received by the chip. Use :py:func:`receive` - to listen, wait and retrieve packets as they're available. - """ - self.operation_mode = RX_MODE - self.dio0_mapping = 0b00 # Interrupt on rx done. - - def transmit(self): - """Transmit a packet which is queued in the FIFO. This is a low level - function for entering transmit mode and more. For generating and - transmitting a packet of data use :py:func:`send` instead. - """ - self.operation_mode = TX_MODE - self.dio0_mapping = 0b01 # Interrupt on tx done. - - @property - def preamble_length(self): - """The length of the preamble for sent and received packets, an unsigned - 16-bit value. Received packets must match this length or they are - ignored! Set to 8 to match the RadioHead RFM95 library. - """ - msb = self._read_u8(_RH_RF95_REG_20_PREAMBLE_MSB) - lsb = self._read_u8(_RH_RF95_REG_21_PREAMBLE_LSB) - return ((msb << 8) | lsb) & 0xFFFF - - @preamble_length.setter - def preamble_length(self, val): - assert 0 <= val <= 65535 - self._write_u8(_RH_RF95_REG_20_PREAMBLE_MSB, (val >> 8) & 0xFF) - self._write_u8(_RH_RF95_REG_21_PREAMBLE_LSB, val & 0xFF) - - @property - def frequency_mhz(self): - """The frequency of the radio in Megahertz. Only the allowed values for - your radio must be specified (i.e. 433 vs. 915 mhz)! - """ - msb = self._read_u8(_RH_RF95_REG_06_FRF_MSB) - mid = self._read_u8(_RH_RF95_REG_07_FRF_MID) - lsb = self._read_u8(_RH_RF95_REG_08_FRF_LSB) - frf = ((msb << 16) | (mid << 8) | lsb) & 0xFFFFFF - frequency = (frf * _RH_RF95_FSTEP) / 1000000.0 - return frequency - - @frequency_mhz.setter - def frequency_mhz(self, val): - if val < 240 or val > 960: - raise RuntimeError("frequency_mhz must be between 240 and 960") - # Calculate FRF register 24-bit value. - frf = int((val * 1000000.0) / _RH_RF95_FSTEP) & 0xFFFFFF - # Extract byte values and update registers. - msb = frf >> 16 - mid = (frf >> 8) & 0xFF - lsb = frf & 0xFF - self._write_u8(_RH_RF95_REG_06_FRF_MSB, msb) - self._write_u8(_RH_RF95_REG_07_FRF_MID, mid) - self._write_u8(_RH_RF95_REG_08_FRF_LSB, lsb) - - @property - def tx_power(self): - """The transmit power in dBm. Can be set to a value from 5 to 23 for - high power devices (RFM95/96/97/98, high_power=True) or -1 to 14 for low - power devices. Only integer power levels are actually set (i.e. 12.5 - will result in a value of 12 dBm). - The actual maximum setting for high_power=True is 20dBm but for values > 20 - the PA_BOOST will be enabled resulting in an additional gain of 3dBm. - The actual setting is reduced by 3dBm. - The reported value will reflect the reduced setting. - """ - if self.high_power: - return self.output_power + 5 - return self.output_power - 1 - - @tx_power.setter - def tx_power(self, val): - val = int(val) - if self.max_output is True: - print("RFM9X Max Output Power Enabled") - self._write_u8(_RH_RF95_REG_0B_OCP, 0x3F) # set Ocp to 240mA - self.pa_dac = _RH_RF95_PA_DAC_ENABLE - self.pa_select = True - self.max_power = 0b111 - self.output_power = 0x0F - return - - if self.high_power: - if val < 5 or val > 23: - raise RuntimeError("tx_power must be between 5 and 23") - # Enable power amp DAC if power is above 20 dB. - # Lower setting by 3db when PA_BOOST enabled - see Data Sheet Section 6.4 - if val > 20: - self.pa_dac = _RH_RF95_PA_DAC_ENABLE - val -= 3 - else: - self.pa_dac = _RH_RF95_PA_DAC_DISABLE - self.pa_select = True - self.output_power = (val - 5) & 0x0F - else: - assert -1 <= val <= 14 - self.pa_select = False - self.max_power = 0b111 # Allow max power output. - self.output_power = (val + 1) & 0x0F - - # ADDED FOR PYCUBED - @property - def packet_status(self): - return (self.rssi, self._read_u8(_RH_RF95_REG_19_PKT_SNR_VALUE) / 4) - - @property - def pll_timeout(self): - return self._read_u8(_RH_RF95_REG_1C_HOP_CHANNEL) - - def rssi(self, raw=False): - """The received strength indicator (in dBm) of the last received message.""" - # Read RSSI register and convert to value using formula in datasheet. - # Remember in LoRa mode the payload register changes function to RSSI! - if raw: - return self._read_u8(_RH_RF95_REG_1A_PKT_RSSI_VALUE) - return self._read_u8(_RH_RF95_REG_1A_PKT_RSSI_VALUE) - 137 - - @property - def signal_bandwidth(self): - """The signal bandwidth used by the radio (try setting to a higher - value to increase throughput or to a lower value to increase the - likelihood of successfully received payloads). Valid values are - listed in RFM9x.bw_bins.""" - bw_id = (self._read_u8(_RH_RF95_REG_1D_MODEM_CONFIG1) & 0xF0) >> 4 - if bw_id >= len(bw_bins): - current_bandwidth = 500000 - else: - current_bandwidth = bw_bins[bw_id] - return current_bandwidth - - @signal_bandwidth.setter - def signal_bandwidth(self, val): - # Set signal bandwidth (set to 125000 to match RadioHead Bw125). - for bw_id, cutoff in enumerate(bw_bins): - if val <= cutoff: - break - else: - bw_id = 9 - self._write_u8( - _RH_RF95_REG_1D_MODEM_CONFIG1, - (self._read_u8(_RH_RF95_REG_1D_MODEM_CONFIG1) & 0x0F) | (bw_id << 4), - ) - if val >= 500000: - # see Semtech SX1276 errata note 2.1 - self._write_u8(0x36, 0x02) - self._write_u8(0x3A, 0x64) - else: - if val == 7800: - self._write_u8(0x2F, 0x48) - elif val >= 62500: - # see Semtech SX1276 errata note 2.3 - self._write_u8(0x2F, 0x40) - else: - self._write_u8(0x2F, 0x44) - self._write_u8(0x30, 0) - - @property - def coding_rate(self): - """The coding rate used by the radio to control forward error - correction (try setting to a higher value to increase tolerance of - short bursts of interference or to a lower value to increase bit - rate). Valid values are limited to 5, 6, 7, or 8.""" - cr_id = (self._read_u8(_RH_RF95_REG_1D_MODEM_CONFIG1) & 0x0E) >> 1 - denominator = cr_id + 4 - return denominator - - @coding_rate.setter - def coding_rate(self, val): - # Set coding rate (set to 5 to match RadioHead Cr45). - denominator = min(max(val, 5), 8) - cr_id = denominator - 4 - self._write_u8( - _RH_RF95_REG_1D_MODEM_CONFIG1, - (self._read_u8(_RH_RF95_REG_1D_MODEM_CONFIG1) & 0xF1) | (cr_id << 1), - ) - - @property - def spreading_factor(self): - """The spreading factor used by the radio (try setting to a higher - value to increase the receiver's ability to distinguish signal from - noise or to a lower value to increase the data transmission rate). - Valid values are limited to 6, 7, 8, 9, 10, 11, or 12.""" - sf_id = (self._read_u8(_RH_RF95_REG_1E_MODEM_CONFIG2) & 0xF0) >> 4 - return sf_id - - @spreading_factor.setter - def spreading_factor(self, val): - # Set spreading factor (set to 7 to match RadioHead Sf128). - val = min(max(val, 6), 12) - self._write_u8(_RH_RF95_DETECTION_OPTIMIZE, 0xC5 if val == 6 else 0xC3) - - if self.signal_bandwidth >= 5000000: - self._write_u8(_RH_RF95_DETECTION_OPTIMIZE, 0xC5 if val == 6 else 0xC3) - else: - # see Semtech SX1276 errata note 2.3 - self._write_u8(_RH_RF95_DETECTION_OPTIMIZE, 0x45 if val == 6 else 0x43) - - self._write_u8(_RH_RF95_DETECTION_THRESHOLD, 0x0C if val == 6 else 0x0A) - self._write_u8( - _RH_RF95_REG_1E_MODEM_CONFIG2, - ( - (self._read_u8(_RH_RF95_REG_1E_MODEM_CONFIG2) & 0x0F) - | ((val << 4) & 0xF0) - ), - ) - - @property - def enable_crc(self): - """Set to True to enable hardware CRC checking of incoming packets. - Incoming packets that fail the CRC check are not processed. Set to - False to disable CRC checking and process all incoming packets.""" - return (self._read_u8(_RH_RF95_REG_1E_MODEM_CONFIG2) & 0x04) == 0x04 - - '''@property - def temperature(self): - """Tries to grab current temp from module""" - raw_temp=self._read_u8(_RH_RF95_REG_3C_REGTEMP) - temp = (raw_temp & 0x7F) - if (raw_temp & 0x80) == 0x80: - temp=~temp+0x01 - - return temp+24#Added prescalar for temp''' - - @property - def former_temperature(self): - """Tries to grab former temp from module""" - raw_temp = self._read_u8(_RH_RF95_REG_5B_FORMER_TEMP) - temp = raw_temp & 0x7F - if (raw_temp & 0x80) == 0x80: - temp = ~temp + 0x01 - - return temp + 143 # Added prescalar for temp - - @enable_crc.setter - def enable_crc(self, val): - # Optionally enable CRC checking on incoming packets. - if val: - self._write_u8( - _RH_RF95_REG_1E_MODEM_CONFIG2, - self._read_u8(_RH_RF95_REG_1E_MODEM_CONFIG2) | 0x04, - ) - else: - self._write_u8( - _RH_RF95_REG_1E_MODEM_CONFIG2, - self._read_u8(_RH_RF95_REG_1E_MODEM_CONFIG2) & 0xFB, - ) - - def tx_done(self): - """Transmit status""" - # if self.dio0: - # print('TxDIO0: {}, {}'.format(self.dio0.value,hex((self._read_u8(_RH_RF95_REG_12_IRQ_FLAGS) & 0x8) >> 3))) - # return self.dio0.value - return (self._read_u8(_RH_RF95_REG_12_IRQ_FLAGS) & 0x8) >> 3 - - def rx_done(self): - """Receive status""" - if self.dio0: - # print('RxDIO0: {}, {}'.format(self.dio0.value,hex((self._read_u8(_RH_RF95_REG_12_IRQ_FLAGS) & 0x40) >> 6))) - return self.dio0.value - else: - return (self._read_u8(_RH_RF95_REG_12_IRQ_FLAGS) & 0x40) >> 6 - - async def await_rx(self, timeout=60): - _t = time.monotonic() + timeout - while not self.rx_done(): - if time.monotonic() < _t: - yield - else: - # Timed out - return False - # Received something - return True - - def crc_error(self): - """crc status""" - return (self._read_u8(_RH_RF95_REG_12_IRQ_FLAGS) & 0x20) >> 5 - - def send( - self, - data, - *, - keep_listening=False, - destination=None, - node=None, - identifier=None, - flags=None - ): - """Send a string of data using the transmitter. - You can only send 252 bytes at a time - (limited by chip's FIFO size and appended headers). - This appends a 4 byte header to be compatible with the RadioHead library. - The header defaults to using the initialized attributes: - (destination,node,identifier,flags) - It may be temporarily overidden via the kwargs - destination,node,identifier,flags. - Values passed via kwargs do not alter the attribute settings. - The keep_listening argument should be set to True if you want to start listening - automatically after the packet is sent. The default setting is False. - - Returns: True if success or False if the send timed out. - """ - # Disable pylint warning to not use length as a check for zero. - # This is a puzzling warning as the below code is clearly the most - # efficient and proper way to ensure a precondition that the provided - # buffer be within an expected range of bounds. Disable this check. - # pylint: disable=len-as-condition - - if hasattr(self, "txrx"): # TX - self.txrx[0].value = True - self.txrx[1].value = False - - l = len(data) - assert 0 < l <= 252 - # pylint: enable=len-as-condition - self.idle() # Stop receiving to clear FIFO and keep it clear. - l += 4 - # Fill the FIFO with a packet to send. - self._write_u8(_RH_RF95_REG_0D_FIFO_ADDR_PTR, 0x00) # FIFO starts at 0. - - # Combine header and data to form payload - if data == b"!": - payload = bytearray(5) - else: - payload = self.buffview[:l] - - if destination is None: # use attribute - payload[0] = self.destination - else: # use kwarg - payload[0] = destination - if node is None: # use attribute - payload[1] = self.node - else: # use kwarg - payload[1] = node - if identifier is None: # use attribute - payload[2] = self.identifier - else: # use kwarg - payload[2] = identifier - if flags is None: # use attribute - payload[3] = self.flags - else: # use kwarg - payload[3] = flags - if self.DEBUG_HEADER: - print("[header] - {}".format([hex(i) for i in payload])) - # payload = payload + data - try: - if isinstance(data, (bytes, bytearray, memoryview)): - payload[4:] = data[:] - else: - payload[4:] = data.encode() - except Exception as e: - print("payload encoding error:", e) - payload = bytearray(payload[:4]) + data - - # Write payload. - self._write_from(_RH_RF95_REG_00_FIFO, payload) - # Write payload and header length. - self._write_u8(_RH_RF95_REG_22_PAYLOAD_LENGTH, l) - # Turn on transmit mode to send out the packet. - self.transmit() - # Wait for tx done interrupt with explicit polling (not ideal but - # best that can be done right now without interrupts). - start = time.monotonic() - timed_out = False - while not timed_out and not self.tx_done(): - if (time.monotonic() - start) >= self.xmit_timeout: - timed_out = True - - if hasattr(self, "txrx"): # RX - self.txrx[0].value = False - self.txrx[1].value = True - - # Listen again if necessary and return the result packet. - if keep_listening: - self.listen() - else: - # Enter idle mode to stop receiving other packets. - self.idle() - # Clear interrupt. - self._write_u8(_RH_RF95_REG_12_IRQ_FLAGS, 0xFF) - return not timed_out - - def send_with_ack(self, data): - """Reliable Datagram mode: - Send a packet with data and wait for an ACK response. - The packet header is automatically generated. - If enabled, the packet transmission will be retried on failure - """ - if self.ack_retries: - retries_remaining = self.ack_retries - else: - retries_remaining = 1 - got_ack = False - self.retry_counter = 0 # ADDED FOR PYCUBED - self.sequence_number = (self.sequence_number + 1) & 0xFF - while not got_ack and retries_remaining: - self.identifier = self.sequence_number - self.send(data, keep_listening=True) - # Don't look for ACK from Broadcast message - if self.destination == _RH_BROADCAST_ADDRESS: - print("uhf destination=RHbroadcast address (dont look for ack)") - got_ack = True - else: - # wait for a packet from our destination - ack_packet = self.receive(timeout=self.ack_wait, with_header=True) - if ack_packet is not None: - if ack_packet[3] & _RH_FLAGS_ACK: - # check the ID - if ack_packet[2] == self.identifier: - got_ack = True - break - # pause before next retry -- random delay - if not got_ack: - self.retry_counter += 1 # ADDED FOR PYCUBED - print("no uhf ack, sending again...") - # delay by random amount before next try - time.sleep(self.ack_wait + self.ack_wait * random()) - retries_remaining = retries_remaining - 1 - # set retry flag in packet header - self.flags |= _RH_FLAGS_RETRY - self.flags = 0 # clear flags - return got_ack - - # pylint: disable=too-many-branches - def receive( - self, - *, - keep_listening=True, - with_header=False, - with_ack=False, - timeout=None, - debug=False, - view=False - ): - """Wait to receive a packet from the receiver. If a packet is found the payload bytes - are returned, otherwise None is returned (which indicates the timeout elapsed with no - reception). - If keep_listening is True (the default) the chip will immediately enter listening mode - after reception of a packet, otherwise it will fall back to idle mode and ignore any - future reception. - All packets must have a 4-byte header for compatibilty with the - RadioHead library. - The header consists of 4 bytes (To,From,ID,Flags). The default setting will strip - the header before returning the packet to the caller. - If with_header is True then the 4 byte header will be returned with the packet. - The payload then begins at packet[4]. - If with_ack is True, send an ACK after receipt (Reliable Datagram mode) - """ - if hasattr(self, "txrx"): # RX - self.txrx[0].value = False - self.txrx[1].value = True - - timed_out = False - if timeout is None: - timeout = self.receive_timeout - if timeout is not None: - # Wait for the payload_ready signal. This is not ideal and will - # surely miss or overflow the FIFO when packets aren't read fast - # enough, however it's the best that can be done from Python without - # interrupt supports. - # Make sure we are listening for packets. - self.listen() - start = time.monotonic() - timed_out = False - while not timed_out and not self.rx_done(): - if (time.monotonic() - start) >= timeout: - timed_out = True - # Payload ready is set, a packet is in the FIFO. - packet = None - # save last RSSI reading - self.last_rssi = self.rssi(raw=True) - # Enter idle mode to stop receiving other packets. - self.idle() - if not timed_out: - if self.enable_crc and self.crc_error(): - self.crc_error_count += 1 - print("crc error") - if hasattr(self, "crc_errs"): - self.crc_errs += 1 - else: - # Read the data from the FIFO. - # Read the length of the FIFO. - fifo_length = self._read_u8(_RH_RF95_REG_13_RX_NB_BYTES) - # Handle if the received packet is too small to include the 4 byte - # RadioHead header and at least one byte of data --reject this packet and ignore it. - if fifo_length > 0: # read and clear the FIFO if anything in it - current_addr = self._read_u8(_RH_RF95_REG_10_FIFO_RX_CURRENT_ADDR) - self._write_u8(_RH_RF95_REG_0D_FIFO_ADDR_PTR, current_addr) - # packet = bytearray(fifo_length) - packet = self.buffview[:fifo_length] - # Read the packet. - self._read_into(_RH_RF95_REG_00_FIFO, packet) - # Clear interrupt. - self._write_u8(_RH_RF95_REG_12_IRQ_FLAGS, 0xFF) - if fifo_length < 5: - print("missing pckt header") - packet = None - else: - if ( - self.node != _RH_BROADCAST_ADDRESS - and packet[0] != _RH_BROADCAST_ADDRESS - and packet[0] != self.node - ): - packet = None - # send ACK unless this was an ACK or a broadcast - elif ( - with_ack - and ((packet[3] & _RH_FLAGS_ACK) == 0) - and (packet[0] != _RH_BROADCAST_ADDRESS) - ): - # delay before sending Ack to give receiver a chance to get ready - if self.ack_delay is not None: - time.sleep(self.ack_delay) - # send ACK packet to sender (data is b'!') - self.send( - b"!", - keep_listening=keep_listening, - destination=packet[1], - node=packet[0], - identifier=packet[2], - flags=(packet[3] | _RH_FLAGS_ACK), - ) - if debug: - print("Sent Ack to {}".format(packet[1])) - if debug: - print("\t{}".format(packet)) - - # # reject Retries if we have seen this idetifier from this source before - # if (self.seen_ids[packet[1]] == packet[2]) and ( - # packet[3] & _RH_FLAGS_RETRY - # ): - # print('duplicate identifier from this source. rejecting...') - # packet = None - # else: # save the packet identifier for this source - self.seen_ids[packet[1]] = packet[2] - if ( - not with_header and packet is not None - ): # skip the header if not wanted - packet = packet[4:] - - if hasattr(self, "txrx"): # RX - self.txrx[0].value = False - self.txrx[1].value = True - - # Listen again if necessary and return the result packet. - if keep_listening: - self.listen() - else: - # Enter idle mode to stop receiving other packets. - self.idle() - # Clear interrupt. - self._write_u8(_RH_RF95_REG_12_IRQ_FLAGS, 0xFF) - if view: - return packet - elif packet is not None: - return bytes(packet) - return packet - - def receive_all(self, only_for_me=True, debug=False): - # msg=[] - l = 0 - fifo_length = 0 - self.idle() - if self.enable_crc and self.crc_error(): - self.crc_error_count += 1 - print("crc error") - if hasattr(self, "crc_errs"): - self.crc_errs += 1 - else: - fifo_length = self._read_u8(_RH_RF95_REG_13_RX_NB_BYTES) - - if fifo_length > 0: - current_addr = self._read_u8(_RH_RF95_REG_10_FIFO_RX_CURRENT_ADDR) - self._write_u8(_RH_RF95_REG_0D_FIFO_ADDR_PTR, _RH_RF95_REG_00_FIFO) - self._read_into(_RH_RF95_REG_00_FIFO, _bigbuffer) - for i in range(4): - self._write_from(_RH_RF95_REG_00_FIFO, bytes(64)) - self._write_u8(_RH_RF95_REG_0D_FIFO_ADDR_PTR, _RH_RF95_REG_00_FIFO) - self.listen() - # Clear interrupt. - self._write_u8(_RH_RF95_REG_12_IRQ_FLAGS, 0xFF) - packetindex = [] - i = 0 - while i < 253: # 256-4 = 252 - # check first - if self.buffview[i] in self.valid_ids: - # check second - if self.buffview[i + 1] in self.valid_ids: - # make sure not the same - if self.buffview[i] != self.buffview[i + 1]: - # append first - packetindex.append(i) - i = i + 4 - l += 1 - continue - i += 1 - for i in range(l - 1): - # assume packets are back-to-back (read till index of next one) - # if (packetindex[i+1]-packetindex[i]) <= 10: - # msg.append(bytes(self.buffview[packetindex[i]:packetindex[i+1]])) - yield self.buffview[packetindex[i] : packetindex[i + 1]] - # last packet so read until the end of our fifo_length - if packetindex: - # if (packetindex[-1]-(current_addr+fifo_length)) <= 10: - # print('{},{},{}'.format(packetindex[-1],current_addr,fifo_length)) - # msg.append(bytes(self.buffview[packetindex[-1]:current_addr+fifo_length])) - yield self.buffview[packetindex[-1] : current_addr + fifo_length] - else: - self.listen() - # Clear interrupt. - self._write_u8(_RH_RF95_REG_12_IRQ_FLAGS, 0xFF) - - # if only_for_me: - # return [i for i in msg if msg[0] is self.node] - # else: - # return msg - - def send_fast(self, data, l): - self.idle() - self._write_u8(_RH_RF95_REG_0D_FIFO_ADDR_PTR, 0x00) # set fifo position - # Write payload. - self._write_from(_RH_RF95_REG_00_FIFO, data) - # Write payload and header length. - self._write_u8(_RH_RF95_REG_22_PAYLOAD_LENGTH, l) - # Turn on transmit mode to send out the packet. - self.transmit() - # Wait for tx done interrupt with explicit polling (not ideal but - # best that can be done right now without interrupts). - _t = time.monotonic() + 5 - while time.monotonic() < _t and not self.tx_done(): - pass - self.idle() - # Clear interrupt. - self._write_u8(_RH_RF95_REG_12_IRQ_FLAGS, 0xFF) - return diff --git a/FC_Board/lib/rv3028.py b/FC_Board/lib/rv3028.py new file mode 100644 index 00000000..fb1605ab --- /dev/null +++ b/FC_Board/lib/rv3028.py @@ -0,0 +1,239 @@ +""" +This class handles communications + +Authors: Nicole Maggard, Michael Pham, and Rachel Sarmiento +""" + +import time +import adafruit_bus_device.i2c_device as i2c_device + + +class RV3028: + # Register addresses + SECONDS = 0x00 + MINUTES = 0x01 + HOURS = 0x02 + WEEKDAY = 0x03 + DATE = 0x04 + MONTH = 0x05 + YEAR = 0x06 + STATUS = 0x0E + CONTROL1 = 0x0F + CONTROL2 = 0x10 + EVENT_CONTROL = 0x13 + TIMESTAMP_COUNT = 0x14 + TIMESTAMP_SECONDS = 0x15 + TIMESTAMP_MINUTES = 0x16 + TIMESTAMP_HOURS = 0x17 + TIMESTAMP_DATE = 0x18 + TIMESTAMP_MONTH = 0x19 + TIMESTAMP_YEAR = 0x1A + EEPROM_BACKUP = 0x37 + + def __init__(self, i2c_bus, address=0x52): + self.i2c_device = i2c_device.I2CDevice(i2c_bus, address) + + def _read_register(self, register, length=1): + with self.i2c_device as i2c: + i2c.write(bytes([register])) + result = bytearray(length) + i2c.readinto(result) + return result + + def _write_register(self, register, data): + with self.i2c_device as i2c: + i2c.write(bytes([register]) + data) + + def _bcd_to_int(self, bcd): + return (bcd & 0x0F) + ((bcd >> 4) * 10) + + def _int_to_bcd(self, value): + return ((value // 10) << 4) | (value % 10) + + def set_time(self, hours, minutes, seconds): + data = bytes( + [ + self._int_to_bcd(seconds), + self._int_to_bcd(minutes), + self._int_to_bcd(hours), + ] + ) + self._write_register(self.SECONDS, data) + + def get_time(self): + data = self._read_register(self.SECONDS, 3) + return ( + self._bcd_to_int(data[2]), # hours + self._bcd_to_int(data[1]), # minutes + self._bcd_to_int(data[0]), # seconds + ) + + def set_date(self, year, month, date, weekday): + data = bytes( + [ + self._int_to_bcd(weekday), + self._int_to_bcd(date), + self._int_to_bcd(month), + self._int_to_bcd(year), + ] + ) + self._write_register(self.WEEKDAY, data) + + def get_date(self): + data = self._read_register(self.WEEKDAY, 4) + return ( + self._bcd_to_int(data[3]), # year + self._bcd_to_int(data[2]), # month + self._bcd_to_int(data[1]), # date + self._bcd_to_int(data[0]), # weekday + ) + + def set_alarm(self, minute, hour, weekday): + # Set alarm mask to check for minute, hour, and weekday match + control2 = self._read_register(self.CONTROL2)[0] + control2 |= 0x08 # Set AIE (Alarm Interrupt Enable) bit + self._write_register(self.CONTROL2, bytes([control2])) + + data = bytes( + [ + self._int_to_bcd(minute), + self._int_to_bcd(hour), + self._int_to_bcd(weekday), + ] + ) + self._write_register(self.MINUTES, data) + + def enable_trickle_charger(self, resistance=3000): + control1 = self._read_register(self.CONTROL1)[0] + control1 |= 0x20 # Set TCE (Trickle Charge Enable) bit + + # Set TCR (Trickle Charge Resistor) bits + if resistance == 3000: + control1 |= 0x00 + elif resistance == 5000: + control1 |= 0x01 + elif resistance == 9000: + control1 |= 0x02 + elif resistance == 15000: + control1 |= 0x03 + else: + raise ValueError("Invalid trickle charger resistance") + + self._write_register(self.CONTROL1, bytes([control1])) + + def disable_trickle_charger(self): + control1 = self._read_register(self.CONTROL1)[0] + control1 &= ~0x20 # Clear TCE (Trickle Charge Enable) bit + self._write_register(self.CONTROL1, bytes([control1])) + + def configure_evi(self, enable=True): + """ + Configure EVI for rising edge detection, enable time stamping, + and enable interrupt. + + :param enable: True to enable EVI, False to disable + """ + if enable: + # Configure Event Control Register + event_control = 0x40 # EHL = 1 (rising edge), ET = 00 (no filtering) + self._write_register(self.EVENT_CONTROL, bytes([event_control])) + + # Enable time stamping and EVI interrupt + control2 = self._read_register(self.CONTROL2)[0] + control2 |= 0x84 # Set TSE (bit 7) and EIE (bit 2) + self._write_register(self.CONTROL2, bytes([control2])) + else: + # Disable time stamping and EVI interrupt + control2 = self._read_register(self.CONTROL2)[0] + control2 &= ~0x84 # Clear TSE (bit 7) and EIE (bit 2) + self._write_register(self.CONTROL2, bytes([control2])) + + def get_event_timestamp(self): + """ + Read the timestamp of the last EVI event. + + :return: Tuple of (year, month, date, hours, minutes, seconds, count) + """ + data = self._read_register(self.TIMESTAMP_COUNT, 7) + return ( + self._bcd_to_int(data[6]), # year + self._bcd_to_int(data[5]), # month + self._bcd_to_int(data[4]), # date + self._bcd_to_int(data[3]), # hours + self._bcd_to_int(data[2]), # minutes + self._bcd_to_int(data[1]), # seconds + data[0], # count (not BCD) + ) + + def clear_event_flag(self): + """ + Clear the Event Flag (EVF) in the Status Register. + """ + status = self._read_register(self.STATUS)[0] + status &= ~0x02 # Clear EVF (bit 1) + self._write_register(self.STATUS, bytes([status])) + + def is_event_flag_set(self): + """ + Check if the Event Flag (EVF) is set in the Status Register. + + :return: True if EVF is set, False otherwise + """ + status = self._read_register(self.STATUS)[0] + return bool(status & 0x02) # Check EVF (bit 1) + + def configure_backup_switchover(self, mode="level", interrupt=False): + """ + Configure the Automatic Backup Switchover function. + + :param mode: 'level' for Level Switching Mode (LSM), + 'direct' for Direct Switching Mode (DSM), + or 'disabled' to disable switchover + :param interrupt: True to enable backup switchover interrupt, False to disable + """ + backup_reg = self._read_register(self.EEPROM_BACKUP)[0] + + # Clear existing BSM bits + backup_reg &= ~0x0C + + if mode == "level": + backup_reg |= 0x0C # Set BSM to 11 for LSM + elif mode == "direct": + backup_reg |= 0x04 # Set BSM to 01 for DSM + elif mode == "disabled": + pass # BSM is already cleared to 00 + else: + raise ValueError("Invalid mode. Use 'level', 'direct', or 'disabled'.") + + # Configure backup switchover interrupt + if interrupt: + backup_reg |= 0x40 # Set BSIE bit + else: + backup_reg &= ~0x40 # Clear BSIE bit + + # Always enable fast edge detection + backup_reg |= 0x10 # Set FEDE bit + + # Write the configuration to EEPROM + self._write_register(self.EEPROM_BACKUP, bytes([backup_reg])) + + # Update EEPROM (command 0x11) + self._write_register(0x27, bytes([0x00])) # First command must be 00h + self._write_register(0x27, bytes([0x11])) # Update command + + def is_backup_switchover_occurred(self): + """ + Check if a backup switchover has occurred. + + :return: True if switchover occurred, False otherwise + """ + status = self._read_register(self.STATUS)[0] + return bool(status & 0x20) # Check BSF (bit 5) + + def clear_backup_switchover_flag(self): + """ + Clear the Backup Switchover Flag (BSF) in the Status Register. + """ + status = self._read_register(self.STATUS)[0] + status &= ~0x20 # Clear BSF (bit 5) + self._write_register(self.STATUS, bytes([status])) diff --git a/FC_Board/main.py b/FC_Board/main.py old mode 100644 new mode 100755 index 329b3231..69d731a6 --- a/FC_Board/main.py +++ b/FC_Board/main.py @@ -1,10 +1,14 @@ """ Created by Nicole Maggard and Michael Pham 8/19/2022 Updated for Yearling by Nicole Maggard and Rachel Sarmiento 2/4/2023 +Updated again for Orpheus by Michael Pham 9/30/2024 This is where the processes get scheduled, and satellite operations are handeled """ from pysquared import cubesat as c + +c.watchdog_pet() + import asyncio import time import traceback @@ -13,188 +17,89 @@ import functions from debugcolor import co +beacon_interval = 15 + def debug_print(statement): if c.debug: - print(co("[MAIN]" + str(statement), "blue", "bold")) + print(co(str(c.uptime) + "[MAIN]" + str(statement), "blue", "bold")) f = functions.functions(c) + + +def initial_boot(): + c.watchdog_pet() + f.beacon() + c.watchdog_pet() + f.listen() + c.watchdog_pet() + # f.state_of_health() + # f.listen() + c.watchdog_pet() + + try: c.c_boot += 1 # Increment boot number debug_print("Boot number: " + str(c.c_boot)) debug_print(str(gc.mem_free()) + " Bytes remaining") - f.beacon() - f.listen() - - f.beacon() - f.listen() - f.state_of_health() - f.listen() + initial_boot() - f.beacon() - f.listen() - f.state_of_health() - f.listen() except Exception as e: debug_print("Error in Boot Sequence: " + "".join(traceback.format_exception(e))) finally: debug_print("Something went wrong!") -def critical_power_operations(): - f.beacon() - f.listen() - f.state_of_health() - f.listen() +def send_imu(): + debug_print("Looking to get imu data...") + IMUData = [] + c.watchdog_pet() + debug_print("IMU has baton") + IMUData = f.get_imu_data() + c.watchdog_pet() + f.send(IMUData) - f.Long_Hybernate() - - -def minimum_power_operations(): +def main(): f.beacon() - f.listen() - f.state_of_health() - f.listen() - - f.Short_Hybernate() + f.listen_loiter() -def normal_power_operations(): - - debug_print("Entering Norm Operations") - FaceData = [] - - # Defining L1 Tasks - def check_power(): - gc.collect() - - print("Implement a New Function Here!") - c.check_reboot() - - if c.power_mode == "normal" or c.power_mode == "maximum": - pwr = True - if c.power_mode == "normal": - c.RGB = (255, 255, 0) - else: - c.RGB = (0, 255, 0) - else: - pwr = False - - debug_print(c.power_mode) - gc.collect() - return pwr - - async def s_lora_beacon(): - - while check_power(): - f.beacon() - f.listen() - f.state_of_health() - f.listen() - time.sleep(1) # Guard Time - - await asyncio.sleep(30) - - async def g_face_data(): - - while check_power(): - try: - print("Pass Consider Adding a New check_power Function Here") - - except Exception as e: - debug_print("Outta time! " + "".join(traceback.format_exception(e))) - - gc.collect() - - await asyncio.sleep(60) - - async def s_face_data(): - - await asyncio.sleep(20) - - while check_power(): - try: - debug_print("Looking to send face data...") - f.send_face() - - except asyncio.TimeoutError as e: - debug_print("Outta time! " + "".join(traceback.format_exception(e))) - - gc.collect() - - await asyncio.sleep(200) - - async def s_imu_data(): - - await asyncio.sleep(45) - - while check_power(): - - try: - debug_print("Looking to get imu data...") - IMUData = [] - - debug_print("IMU has baton") - IMUData = f.get_imu_data() - f.send(IMUData) - f.face_data_baton = False - - except Exception as e: - debug_print("Outta time! " + "".join(traceback.format_exception(e))) + f.state_of_health() - gc.collect() + f.listen_loiter() - await asyncio.sleep(100) + f.all_face_data() + c.watchdog_pet() + f.send_face() - async def detumble(): + f.listen_loiter() - await asyncio.sleep(300) + send_imu() - while check_power(): - try: - debug_print("Looking to detumble...") - f.detumble() - debug_print("Detumble complete") + f.listen_loiter() - except Exception as e: - debug_print(f"Outta time!" + "".join(traceback.format_exception(e))) + f.joke() - gc.collect() + f.listen_loiter() - await asyncio.sleep(300) - async def joke(): - await asyncio.sleep(500) +def critical_power_operations(): - while check_power(): - try: - debug_print("Joke send go!") - f.joke() - if f.listen_joke(): - f.joke() - debug_print("done!") - except Exception as e: - debug_print(f"Outta time!" + "".join(traceback.format_exception(e))) + initial_boot() + c.watchdog_pet() - gc.collect() - await asyncio.sleep(500) + f.Long_Hybernate() - async def main_loop(): - # log_face_data_task = asyncio.create_task(l_face_data()) - t1 = asyncio.create_task(s_lora_beacon()) - t2 = asyncio.create_task(s_face_data()) - t3 = asyncio.create_task(s_imu_data()) - t4 = asyncio.create_task(g_face_data()) - t5 = asyncio.create_task(detumble()) - t6 = asyncio.create_task(joke()) +def minimum_power_operations(): - await asyncio.gather(t1, t2, t3, t4, t5, t6) + initial_boot() + c.watchdog_pet() - asyncio.run(main_loop()) + f.Short_Hybernate() ######################### MAIN LOOP ############################## @@ -214,17 +119,17 @@ async def main_loop(): elif c.power_mode == "normal": c.RGB = (255, 255, 0) - normal_power_operations() + main() elif c.power_mode == "maximum": c.RGB = (0, 255, 0) - normal_power_operations() + main() else: f.listen() except Exception as e: - debug_print("Error in Main Loop: " + "".join(traceback.format_exception(e))) + debug_print("Critical in Main Loop: " + "".join(traceback.format_exception(e))) time.sleep(10) microcontroller.on_next_reset(microcontroller.RunMode.NORMAL) microcontroller.reset() @@ -232,3 +137,4 @@ async def main_loop(): debug_print("Going Neutral!") c.RGB = (0, 0, 0) + c.hardware["WDT"] = False diff --git a/FC_Board/repl.py b/FC_Board/repl.py new file mode 100644 index 00000000..1281d3df --- /dev/null +++ b/FC_Board/repl.py @@ -0,0 +1,7 @@ +import board +import time +import digitalio + +watchdog_pin = digitalio.DigitalInOut(board.WDT_WDI) +watchdog_pin.direction = digitalio.Direction.OUTPUT +watchdog_pin.value = False diff --git a/Tests/fsk_test.py b/Tests/fsk_test.py new file mode 100644 index 00000000..e4c618d6 --- /dev/null +++ b/Tests/fsk_test.py @@ -0,0 +1,61 @@ +import board +import busio +import digitalio +import time +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/radio_test.py b/Tests/radio_test.py old mode 100644 new mode 100755 index 71f7a174..ffc4e71d --- a/Tests/radio_test.py +++ b/Tests/radio_test.py @@ -6,10 +6,16 @@ import time from pysquared import cubesat +from time import sleep 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 = { @@ -21,7 +27,15 @@ "enable_crc": False, } -options = ["A", "B"] +if input("FSK or LoRa? [L/f]") == "F": + cubesat.f_fsk = True + del cubesat + print("Resetting in FSK") + from pysquared import cubesat + +print("FSK: " + str(cubesat.f_fsk)) + +options = ["A", "B", "C"] # Setting the Radio cubesat.radio1.spreading_factor = radio_cfg["spreading_factor"] @@ -44,6 +58,9 @@ | Please Select Your Node | | 'A': Device Under Test | | 'B': Receiver | +================ OR =================== +| Act as a client | +| 'C': for an active satalite | ======================================= """ ) @@ -60,7 +77,7 @@ def device_under_test(attempts): debug_print("Setting up Radio...") cubesat.radio1.node = 0xFA - cubesat.radio1.destination = 0xFF + cubesat.radio1.destination = 0xFB debug_print("Radio Setup Complete") debug_print("Sending Ping...") @@ -71,7 +88,7 @@ def device_under_test(attempts): debug_print("Ping Sent") debug_print("Awaiting Response...") - heard_something = cubesat.radio1.await_rx(timeout=10) + heard_something = cubesat.radio1.receive(timeout=10) if heard_something: handle_ping() @@ -88,13 +105,13 @@ def receiver(): debug_print("Receiver Selected") debug_print("Setting up Radio...") - cubesat.radio1.node = 0xFF - cubesat.radio1.destination = 0xFA + cubesat.radio1.node = 0xFA + cubesat.radio1.destination = 0xFB debug_print("Radio Setup Complete") debug_print("Awaiting Ping...") - heard_something = cubesat.radio1.await_rx(timeout=10) + heard_something = cubesat.radio1.receive(timeout=10) if heard_something: handle_ping() @@ -106,6 +123,96 @@ def receiver(): 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) @@ -159,6 +266,12 @@ def handle_ping(): """ ) +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": @@ -168,3 +281,6 @@ def handle_ping(): elif device_selection == "B": time.sleep(1) receiver() + elif device_selection == "C": + client(passcode) + time.sleep(1) diff --git a/experimental/lib/packet_receiver.py b/experimental/lib/packet_receiver.py new file mode 100644 index 00000000..89c34063 --- /dev/null +++ b/experimental/lib/packet_receiver.py @@ -0,0 +1,381 @@ +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)