From ce9b7c4760d04bc514e9167ff4b8b94be44b0fc5 Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:40:50 -0800 Subject: [PATCH] V.1.2.0 | 33 bring big data back (#49) --- Batt_Board/can_test.py | 6 +- Batt_Board/lib/adafruit_mcp2515/__init__.py | 4 + Batt_Board/lib/battery_functions.py | 18 - FC_Board/code.py | 4 +- {Batt_Board => FC_Board}/lib/Big_Data.py | 26 +- FC_Board/lib/adafruit_mcp2515/__init__.py | 4 + FC_Board/lib/adafruit_tca9548a.mpy | Bin 1397 -> 0 bytes .../lib/adafruit_tca9548a.py | 4 +- FC_Board/lib/battery_helper.py | 292 +++++++++++ FC_Board/lib/can_bus_helper.py | 462 ++++++++++++++++++ FC_Board/lib/functions.py | 86 +++- FC_Board/lib/packet_manager.py | 117 +++++ FC_Board/lib/packet_sender.py | 165 +++++++ FC_Board/lib/pysquared.py | 91 ++-- FC_Board/main.py | 76 ++- FC_Board/repl.py | 8 +- experimental/lib/packet_receiver.py | 381 +++++++++++++++ 17 files changed, 1635 insertions(+), 109 deletions(-) rename {Batt_Board => FC_Board}/lib/Big_Data.py (96%) delete mode 100755 FC_Board/lib/adafruit_tca9548a.mpy 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 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/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/battery_functions.py b/Batt_Board/lib/battery_functions.py index 9622dd23..a5d7aaee 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/FC_Board/code.py b/FC_Board/code.py index 86fff930..c863b5c7 100644 --- a/FC_Board/code.py +++ b/FC_Board/code.py @@ -5,7 +5,7 @@ """ Built for the PySquared FC Board Version: 1.2.0 (Beta) -Published: October 31, 2024 +Published: Nov 12, 2024 """ import time @@ -13,7 +13,7 @@ print("=" * 70) print("Hello World!") print("PySquared FC Board Circuit Python Software Version: 1.2.0 (Beta)") -print("Published: October 31, 2024") +print("Published: November 12, 2024") print("=" * 70) loiter_time = 5 diff --git a/Batt_Board/lib/Big_Data.py b/FC_Board/lib/Big_Data.py similarity index 96% rename from Batt_Board/lib/Big_Data.py rename to FC_Board/lib/Big_Data.py index bb831820..91bf89e0 100644 --- a/Batt_Board/lib/Big_Data.py +++ b/FC_Board/lib/Big_Data.py @@ -8,14 +8,20 @@ from debugcolor import co import time -import board -import busio import traceback +import gc + +print(gc.mem_free()) import adafruit_mcp9808 # temperature sensor -import adafruit_tca9548a # I2C multiplexer + +print(gc.mem_free()) import adafruit_veml7700 # light sensor + +print(gc.mem_free()) import adafruit_drv2605 # Coil motor driver +print(gc.mem_free()) + # Is the Face cass even necessary? class Face: @@ -168,8 +174,8 @@ def drive(self, sequence): 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): + # Function takes number of tests "num" + def test_all(self, num): self.datalist = [] self.debug_print("Expected Sensors: " + str(self.senlist_what)) self.debug_print("Initialized Sensors: " + str(self.active_sensors)) @@ -341,11 +347,11 @@ 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)) + self.BigFaceList.append(self.Face0.test_all(1)) + self.BigFaceList.append(self.Face1.test_all(1)) + self.BigFaceList.append(self.Face2.test_all(1)) + self.BigFaceList.append(self.Face3.test_all(1)) + self.BigFaceList.append(self.Face4.test_all(1)) for face in self.BigFaceList: self.debug_print(str(face)) diff --git a/FC_Board/lib/adafruit_mcp2515/__init__.py b/FC_Board/lib/adafruit_mcp2515/__init__.py index b6a54ed0..45490f6c 100644 --- a/FC_Board/lib/adafruit_mcp2515/__init__.py +++ b/FC_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/FC_Board/lib/adafruit_tca9548a.mpy b/FC_Board/lib/adafruit_tca9548a.mpy deleted file mode 100755 index d6065b2101817c87a197482f3a90f43f17856e71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1397 zcmb7B-E!JS7+neTV@ib(bS)d%mcfy2ejOR()HlV3Nz#!5za4!&^ez)ILR9(SGwYz4crAgC~t}n$r@K!o_2;h(N2V)ZlQobQ;8fpT1 zRsr<$370(pQ5W_KIK`Go#w*oS8h|$SuGDNPKLW4OM5qm1s;B_!)!3BT0+d{>>c}vF zucJ*=OFDK}(_0sks$o7h6-s6UY0``X9z&5eKu^yyz}D*5P?BCEeSBw00&;w+yO?g1_W0Owu-y;K#%PcO`0bPSWPAik=OUj=CR2RbKL-9|k)&x1=#t{NDuM_# zczDiN`M$fFtDaZS`~E-_uG18JAOaW=2^bPdxF|Z{k~jw!L?;Z3F36P>cke7t1q5cp z%C3)wEW>Q}ZvKuF8Dhu=OUf0yMu!AzpO5T_hff(Y^qG|<%EKTNgrQIt6p}Vv7%%KF z7|fF87s@5O=35l31ODNCHcDdBE-Yq&a@qLJ$}vjCzT3BMPfFtMm3Lm*qY zG`cx@W9?m%Q~APJUi_$h;ZFYguYN8R7T9>gI?H`*e0X&8=IAY+?@)T4fR__2nYRz- z#tsu6%0H;(e#liPQTpM7h}Dh}M>}?)D>%?q9Dc9yD?tYY$Xe?9I6`Crp8Mt9;o-OC{1YY>otxZxd2~|xqwp0& u4M!Pr7+fBGGya`S%+Q1Gtr_}1&HVfiGyVH!di`57{d;Dv?#wv<)yxM$Sb-k^ 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..215d2357 --- /dev/null +++ b/FC_Board/lib/battery_helper.py @@ -0,0 +1,292 @@ +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 UnicodeDecodeError: + 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 c7da6e5d..f39afa05 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,10 +24,15 @@ 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?" ] @@ -43,6 +51,23 @@ 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() + """ Radio Functions """ @@ -67,6 +92,15 @@ 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 @@ -113,13 +147,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)}", @@ -231,21 +265,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 +314,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 +326,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 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 162a7030..3aec1029 100755 --- a/FC_Board/lib/pysquared.py +++ b/FC_Board/lib/pysquared.py @@ -17,6 +17,7 @@ from bitflags import bitFlag, multiBitFlag, multiByte from micropython import const from debugcolor import co +from collections import OrderedDict # Hardware Specific Libs import pysquared_rfm9x # Radio @@ -34,7 +35,7 @@ # NVM register numbers _BOOTCNT = const(0) _VBUSRST = const(6) -_STATECNT = const(7) +_ERRORCNT = const(7) _TOUTS = const(9) _ICHRG = const(11) _DIST = const(13) @@ -51,7 +52,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) @@ -73,6 +74,7 @@ def debug_print(self, statement): 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")) @@ -83,6 +85,7 @@ 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.orpheus = True # Define if the board is used with Orpheus or not self.is_licensed = False """ @@ -105,12 +108,19 @@ 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 """ @@ -130,27 +140,29 @@ def __init__(self): "pwr": 23, "st": 80000, } - self.hardware = { - "I2C0": False, - "SPI0": False, - "I2C1": False, - "UART": False, - "IMU": False, - "Mag": False, - "Radio1": False, - "SDcard": False, - "NEOPIX": False, - "WDT": False, - "TCA": False, - "CAN": False, - "RTC": False, - "Face0": False, - "Face1": False, - "Face2": False, - "Face3": False, - "Face4": False, - "CAM": False, - } + self.hardware = OrderedDict( + [ + ("I2C0", False), + ("SPI0", False), + ("I2C1", False), + ("UART", False), + ("Radio1", False), + ("IMU", False), + ("Mag", False), + ("SDcard", False), + ("NEOPIX", False), + ("WDT", False), + ("TCA", False), + ("CAN", False), + ("Face0", False), + ("Face1", False), + ("Face2", False), + ("Face3", False), + ("Face4", False), + ("CAM", False), + ("RTC", False), + ] + ) """ NVM Parameter Resets @@ -182,8 +194,11 @@ def __init__(self): 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( @@ -209,8 +224,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( @@ -315,7 +337,6 @@ def __init__(self): # Still need to test these configs self.rtc.configure_backup_switchover(mode="level", interrupt=True) - self.rtc.configure_evi(enable=True, timestamp_mode="last") self.hardware["RTC"] = True except Exception as e: @@ -412,8 +433,6 @@ def __init__(self): self.cam.night_mode = False self.cam.quality = 20 - self.buffer_size = self.cam.height * self.cam.width // self.cam.quality - self.hardware["CAM"] = True except Exception as e: @@ -561,15 +580,14 @@ def reset_vbus(self): if self.hardware["SDcard"]: try: umount("/sd") - self.spi.deinit() + self.spi1.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)) @@ -590,7 +608,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: @@ -648,6 +666,7 @@ def date(self, year, month, date, weekday): 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) @@ -671,7 +690,7 @@ def take_image(self): def watchdog_pet(self): self.watchdog_pin.value = True - time.sleep(0.1) + time.sleep(0.01) self.watchdog_pin.value = False def check_reboot(self): diff --git a/FC_Board/main.py b/FC_Board/main.py index be1cbd74..d15b8fc2 100644 --- a/FC_Board/main.py +++ b/FC_Board/main.py @@ -20,28 +20,24 @@ def debug_print(statement): print(co("[MAIN]" + str(statement), "blue", "bold")) -f = functions.functions(c) -try: - debug_print("Boot number: " + str(c.c_boot)) - debug_print(str(gc.mem_free()) + " Bytes remaining") - +def inital_boot(): c.watchdog_pet() f.beacon() - f.listen() - c.watchdog_pet() - f.beacon() - f.listen() - f.state_of_health() f.listen() - c.watchdog_pet() - f.beacon() - f.listen() f.state_of_health() f.listen() c.watchdog_pet() + +f = functions.functions(c) +try: + debug_print("Boot number: " + str(c.c_boot)) + debug_print(str(gc.mem_free()) + " Bytes remaining") + + inital_boot() + except Exception as e: debug_print("Error in Boot Sequence: " + "".join(traceback.format_exception(e))) finally: @@ -74,14 +70,19 @@ def minimum_power_operations(): 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() + print("Checking Power State") + + print(c.battery_voltage) + print(c.draw_current) + print(c.charge_voltage) + print(c.charge_current) + print(c.is_charging) + print(c.battery_percentage) if c.power_mode == "normal" or c.power_mode == "maximum": pwr = True @@ -113,7 +114,8 @@ async def g_face_data(): while check_power(): try: - print("Pass Consider Adding a New check_power Function Here") + debug_print("Consider Adding a Logging Function Here!") + f.all_face_data() except Exception as e: debug_print("Outta time! " + "".join(traceback.format_exception(e))) @@ -122,6 +124,35 @@ async def g_face_data(): await asyncio.sleep(60) + async def g_batt_data(): + + while check_power(): + try: + debug_print("Looking to get battery data...") + batt_data = f.get_battery_data() + + debug_print("Battery Data: " + str(batt_data)) + + debug_print(batt_data[0]) + debug_print(batt_data[1]) + debug_print(batt_data[2]) + + c.battery_voltage = batt_data[0] + c.draw_current = batt_data[1] + c.charge_voltage = batt_data[2] + c.charge_current = batt_data[3] + c.is_charging = batt_data[4] + c.battery_percentage = batt_data[5] + + c.check_reboot() + + except Exception as e: + debug_print("Outta time! " + "".join(traceback.format_exception(e))) + + gc.collect() + + await asyncio.sleep(30) + async def s_face_data(): await asyncio.sleep(20) @@ -208,11 +239,12 @@ async def main_loop(): 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()) - t7 = asyncio.create_task(check_watchdog()) + t5 = asyncio.create_task(g_batt_data()) + t6 = asyncio.create_task(detumble()) + t7 = asyncio.create_task(joke()) + t8 = asyncio.create_task(check_watchdog()) - await asyncio.gather(t1, t2, t3, t4, t5, t6, t7) + await asyncio.gather(t1, t2, t3, t4, t5, t6, t7, t8) asyncio.run(main_loop()) @@ -244,7 +276,7 @@ async def main_loop(): 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() diff --git a/FC_Board/repl.py b/FC_Board/repl.py index ca1769a4..1281d3df 100644 --- a/FC_Board/repl.py +++ b/FC_Board/repl.py @@ -1 +1,7 @@ -from pysquared import cubesat as c +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/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)