Source code for pycubed

"""
CircuitPython driver for PyCubed-Mini
"""

import sdcardio
import pycubed_rfm9x
import board
import microcontroller
import busio
import digitalio
import analogio
import storage
import sys
import neopixel
import pwmio
import bmx160
import drv8830
from adafruit_pcf8523 import PCF8523
from bitflags import bitFlag, multiBitFlag
from micropython import const
import adafruit_tsl2561
import time
import tasko
from ulab.numpy import array

[docs]class device: """ Based on the code from: https://docs.python.org/3/howto/descriptor.html#properties Attempts to return the appropriate hardware device. If this fails, it will attempt to reinitialize the hardware. If this fails again, it will raise an exception. """ def __init__(self, fget=None): self.fget = fget self._device = None def __get__(self, instance, owner=None): if instance is None: return self if self.fget is None: raise AttributeError(f'unreadable attribute {self._name}') if self._device is not None: return self._device else: self._device = self.fget(instance) return self._device
""" Define constants, Satellite attributes and Satellite Class """ # NVM register numbers _FLAG = const(20) _DWNLINK = const(4) _DCOUNT = const(3) _RSTERRS = const(2) _BOOTCNT = const(0) _LOGFAIL = const(5)
[docs]class _Satellite: # Define NVM flags f_contact = bitFlag(register=_FLAG, bit=1) f_burn = bitFlag(register=_FLAG, bit=2) f_free1 = bitFlag(register=_FLAG, bit=3) f_free2 = bitFlag(register=_FLAG, bit=4) # Define NVM counters c_boot = multiBitFlag(register=_BOOTCNT, lowest_bit=0, num_bits=8) c_state_err = multiBitFlag(register=_RSTERRS, lowest_bit=4, num_bits=4) c_vbus_rst = multiBitFlag(register=_RSTERRS, lowest_bit=0, num_bits=4) c_deploy = multiBitFlag(register=_DCOUNT, lowest_bit=0, num_bits=8) c_downlink = multiBitFlag(register=_DWNLINK, lowest_bit=0, num_bits=8) c_logfail = multiBitFlag(register=_LOGFAIL, lowest_bit=0, num_bits=8) UHF_FREQ = 433.0 instance = None data_cache = {} # Satellite attributes LOW_VOLTAGE = 3.0 # Max opperating temp on specsheet for ATSAMD51J19A (Celsius) HIGH_TEMP = 125 # Min opperating temp on specsheet for ATSAMD51J19A (Celsius) LOW_TEMP = -40 def __new__(cls): """ Override the built-in __new__ function Ensure only one instance of this class can be made per process """ if not cls.instance: cls.instance = object.__new__(cls) cls.instance = super(_Satellite, cls).__new__(cls) return cls.instance def __init__(self): """ Big init routine as the whole board is brought up. """ self.BOOTTIME = int(time.monotonic()) # get monotonic time at initialization self.micro = microcontroller self._vbatt = analogio.AnalogIn(board.BATTERY) # Battery voltage # To force initialization of hardware self.i2c1 self.i2c2 self.i2c3 self.spi self.sdcard self.vfs self.neopixel self.imu self.rtc self.radio self.sun_xn self.sun_yn self.sun_zn self.sun_xp self.sun_yp self.sun_zp self.drv_x self.drv_y self.drv_z self.burnwire1 @device def i2c1(self): """ Initialize I2C1 bus """ try: return busio.I2C(board.SCL1, board.SDA1) except Exception as e: print("[ERROR][Initializing I2C1]", e) @device def i2c2(self): """ Initialize I2C2 bus """ try: return busio.I2C(board.SCL2, board.SDA2) except Exception as e: print("[ERROR][Initializing I2C2]", e) @device def i2c3(self): """ Initialize I2C3 bus """ try: return busio.I2C(board.SCL3, board.SDA3) except Exception as e: print("[ERROR][Initializing I2C3]", e) @device def spi(self): """ Initialize SPI bus """ try: return busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) except Exception as e: print("[ERROR][Initializing SPI]", e) @device def sdcard(self): """ Define SD Parameters and initialize SD Card """ try: return sdcardio.SDCard(self.spi, board.CS_SD, baudrate=4000000) except Exception as e: print('[ERROR][Initializing SD Card]', e) @device def vfs(self): try: vfs = storage.VfsFat(self.sdcard) storage.mount(vfs, "/sd") sys.path.append("/sd") return vfs except Exception as e: print('[ERROR][Initializing VFS]', e) @device def neopixel(self): """ Define neopixel parameters and initialize """ try: led = neopixel.NeoPixel( board.NEOPIXEL, 1, brightness=0.2, pixel_order=neopixel.GRB) led[0] = (0, 0, 0) return led except Exception as e: print('[ERROR][Initializing Neopixel]', e) @device def imu(self): """ Define IMU parameters and initialize """ try: return bmx160.BMX160_I2C(self.i2c1, address=0x69) except Exception as e: print(f'[ERROR][Initializing IMU] {e}\n\tMaybe try address=0x68?') @device def radio(self): """ Define radio parameters and initialize UHF radio """ try: self._rf_cs = digitalio.DigitalInOut(board.RF_CS) self._rf_rst = digitalio.DigitalInOut(board.RF_RST) self.radio_DIO0 = digitalio.DigitalInOut(board.RF_IO0) self.radio_DIO0.switch_to_input() self.radio_DIO1 = digitalio.DigitalInOut(board.RF_IO1) self.radio_DIO1.switch_to_input() self._rf_cs.switch_to_output(value=True) self._rf_rst.switch_to_output(value=True) except Exception as e: print('[ERROR][Initializing Radio]', e) try: radio = pycubed_rfm9x.RFM9x( self.spi, self._rf_cs, self._rf_rst, self.UHF_FREQ) radio.dio0 = self.radio_DIO0 radio.node = 0xAB # our ID radio.destination = 0xBA # target's ID radio.sleep() return radio except Exception as e: print('[ERROR][Initializing RADIO]', e) @device def sun_yn(self): """ Initialize the -Y sun sensor on I2C2 """ try: return adafruit_tsl2561.TSL2561(self.i2c3, address=0x29) except Exception as e: print('[ERROR][Initializing Sun Sensor -Y]', e) @device def sun_zn(self): """ Initialize the -Z sun sensor on I2C2 """ try: return adafruit_tsl2561.TSL2561(self.i2c3, address=0x39) except Exception as e: print('[ERROR][Initializing Sun Sensor -Z]', e) @device def sun_xn(self): """ Initialize the -X sun sensor on I2C1 """ try: return adafruit_tsl2561.TSL2561(self.i2c2, address=0x49) except Exception as e: print('[ERROR][Initializing Sun Sensor -X]', e) @device def sun_yp(self): """ Initialize the +Y sun sensor on I2C1 """ try: return adafruit_tsl2561.TSL2561(self.i2c3, address=0x49) except Exception as e: print('[ERROR][Initializing Sun Sensor +Y]', e) @device def sun_zp(self): """ Initialize the +Z sun sensor on I2C1 """ try: return adafruit_tsl2561.TSL2561(self.i2c2, address=0x39) except Exception as e: print('[ERROR][Initializing Sun Sensor +Z]', e) @device def sun_xp(self): """ Initialize the +X sun sensor on I2C2 """ try: return adafruit_tsl2561.TSL2561(self.i2c2, address=0x29) except Exception as e: print('[ERROR][Initializing Sun Sensor +X]', e) @device def drv_x(self): """ Initialize Coil Driver X on I2C3, set mode and voltage """ try: return drv8830.DRV8830(self.i2c1, 0xC4 >> 1) # U7 except Exception as e: print('[ERROR][Initializing H-Bridge U7]', e) @device def drv_y(self): """ Initialize Coil Driver Y on I2C3, set mode and voltage """ try: return drv8830.DRV8830(self.i2c1, 0xC0 >> 1) # U8 except Exception as e: print('[ERROR][Initializing H-Bridge U8]', e) @device def drv_z(self): """ Initialize Coil Driver Z on I2C3, set mode and voltage """ try: return drv8830.DRV8830(self.i2c1, 0xD0 >> 1) # U9 except Exception as e: print('[ERROR][Initializing H-Bridge U9]', e) @device def burnwire1(self): """ Initialize Burnwire1 on PA19 """ # TODO: update firmware so we can use board.BURN1 try: # changed pinout from BURN1 to PA19 (BURN1 did not support PWMOut) return pwmio.PWMOut( microcontroller.pin.PA19, frequency=1000, duty_cycle=0) except Exception as e: print('[ERROR][Initializing Burn Wire IC1]', e) @device def rtc(self): """ Initialize Real Time Clock """ try: return PCF8523(self.i2c2) except Exception as e: print('[ERROR][Initializing RTC]', e) def imuToBodyFrame(self, vec): return array([-vec[0], vec[2], vec[1]]) @property def acceleration(self): """ return the accelerometer reading from the IMU in m/s^2 """ return self.imuToBodyFrame(self.imu.accel) if self.imu else None @property def magnetic(self): """ return the magnetometer reading from the IMU in µT """ return self.imuToBodyFrame(self.imu.mag) if self.imu else None @property def gyro(self): """ return the gyroscope reading from the IMU in deg/s """ return self.imuToBodyFrame(self.imu.gyro) if self.imu else None @property def temperature_imu(self): """ return the thermometer reading from the IMU in celsius """ return self.imu.temperature if self.imu else None @property def temperature_cpu(self): """ return the temperature reading from the CPU in celsius """ return self.micro.cpu.temperature if self.micro else None
[docs] def coildriver_vout(self, driver_index, projected_voltage): """ Set a given voltage for a given coil driver """ if driver_index == "X" or driver_index == "U7": self.drv_x.throttle_volts = projected_voltage elif driver_index == "Y" or driver_index == "U8": self.drv_y.throttle_volts = projected_voltage elif driver_index == "Z" or driver_index == "U9": self.drv_z.throttle_volts = projected_voltage else: print(driver_index, "is not a defined coil driver")
@property def battery_voltage(self): """ Return the battery voltage _cubesat._vbatt.value converts the analog value of the board.BATTERY pin to a digital one. We read this value 50 times and then later average it to get as close as possible to a reliable battery voltage value """ # initialize vbat vbat = 0 # get the battery value 50 times for _ in range(50): # 65536 = 2^16, number of increments we can have to voltage vbat += self._vbatt.value * 3.3 / 65536 # vbat / 50 = average of all battery voltage values read # 100k/100k voltage divider voltage = (vbat / 50) * (100 + 100) / 100 # volts return voltage @property def sun_vector(self): """Returns the sun pointing vector in the body frame""" return array( [self.sun_xp.lux - self.sun_xn.lux, self.sun_yp.lux - self.sun_yn.lux, self.sun_zp.lux - self.sun_zn.lux])
[docs] async def burn(self, dutycycle=0.5, duration=1): """ Activates the burnwire for a given duration and dutycycle. :param dutycycle: The dutycycle of the burnwire, between 0 and 1 :type dutycycle: float :param duration: The duration of the burn, in seconds :type duration: float :return: True if the burn was successful, False otherwise :rtype: bool """ try: burnwire = self.burnwire1 self.RGB = (255, 0, 0) # set the burnwire's dutycycle; begins the burn burnwire.duty_cycle = int(dutycycle * (0xFFFF)) await tasko.sleep(duration) # wait for given duration # set burnwire's dutycycle back to 0; ends the burn burnwire.duty_cycle = 0 self.RGB = (0, 0, 0) self.f_burn = True return True # burnwire.deinit() # deinitialize burnwire except Exception as e: print('[ERROR][Burning]', e) return False
@property def RGB(self): return self.neopixel[0] @RGB.setter def RGB(self, v): self.neopixel[0] = v
[docs] def timeon(self): """ return the time on a monotonic clock """ return int(time.monotonic()) - self.BOOTTIME
[docs] def reset_boot_count(self): """ reset boot count in non-volatile memory (nvm) """ self.c_boot = 0
[docs] def incr_logfail_count(self): """ increment logfail count in non-volatile memory (nvm) """ self.c_logfail += 1
[docs] def reset_logfail_count(self): """ reset logfail count in non-volatile memory (nvm) """ self.c_logfail = 0
# initialize Satellite as cubesat cubesat = _Satellite()