diff --git a/gs_commands.py b/gs_commands.py index 77e0170..c92a45c 100644 --- a/gs_commands.py +++ b/gs_commands.py @@ -117,6 +117,21 @@ async def request_beacon(radio, debug=False): else: return False, None +async def request_image(radio, debug=False): + # Notice: response is never used + success, header, response = await send_command( + radio, + commands_by_name["REQUEST_IMAGE"]["bytes"], + "", + commands_by_name["REQUEST_IMAGE"]["will_respond"], + debug=debug) + if success and (header == headers.IMAGE_START or + header == headers.IMAGE_MID or + header == headers.IMAGE_END + ): + return True + else: + return False async def set_time(radio, unix_time=None, debug=False): """ Update the real time clock on the satellite using either a given value or the system time""" @@ -202,12 +217,14 @@ def __init__(self): self.msg_last = bytes([]) self.cmsg = bytes([]) self.cmsg_last = bytes([]) + self.current_time = '' # Only used for images async def wait_for_message(radio, max_rx_fails=10, debug=False): data = _data() rx_fails = 0 + # This while loop never goes over one iteration. Possibly irrelevant? while True: res = await receive(radio, debug=debug) @@ -227,6 +244,7 @@ async def wait_for_message(radio, max_rx_fails=10, debug=False): oh = header[5] if oh == headers.DEFAULT or oh == headers.BEACON: return oh, payload + elif oh == headers.MEMORY_BUFFERED_START or oh == headers.MEMORY_BUFFERED_MID or oh == headers.MEMORY_BUFFERED_END: handle_memory_buffered(oh, data, payload) if oh == headers.MEMORY_BUFFERED_END: @@ -236,6 +254,12 @@ async def wait_for_message(radio, max_rx_fails=10, debug=False): handle_disk_buffered(oh, data, payload) if oh == headers.DISK_BUFFERED_END: return headers.DISK_BUFFERED_START, data.cmsg + + elif oh == headers.IMAGE_START or oh == headers.IMAGE_MID or oh == headers.IMAGE_END: + handle_image(oh, data, payload) + if oh == headers.IMAGE_END: + return headers.IMAGE_START, data.cmsg + else: print(f"Unrecognized header {oh}") return oh, payload @@ -301,3 +325,29 @@ def handle_disk_buffered(header, data, response): if header == headers.DISK_BUFFERED_END: data.cmsg_last = bytes([]) + +def handle_image(header, data, payload): + if header == headers.IMAGE_START: + data.cmsg = payload + data.cmsg_last = payload + try: + # Time is in the format MM/DD/YY_HOUR:MIN:SEC + data.current_time = time.strftime('%x_%X', time.localtime()) + with open(f'{data.current_time}_satellite_image.jpeg', 'wb') as fd: + fd.write(payload) + except Exception as e: + print(f'Failed to write image: {e}') + else: + if payload != data.cmsg_last: + data.cmsg += payload + try: + with open(f'{data.current_time}_satellite_image.jpeg', 'ab') as fd: + fd.write(payload) + except Exception as e: + print(f'Failed to write to image: {e}') + else: + print('Repeated payload') + data.cmsg_last = payload + + if header == headers.IMAGE_END: + data.cmsg_last = bytes([]) \ No newline at end of file diff --git a/gs_shell.py b/gs_shell.py index 05695fd..011e607 100644 --- a/gs_shell.py +++ b/gs_shell.py @@ -22,6 +22,7 @@ prompt_options = {"Receive loop": ("r", "receive"), "Beacon request loop": ("b", "beacon"), + "Image request loop": ("i", "image"), "Upload file": ("u", "upload"), "Request file": ("rf", "request"), "Send command": ("c", "command"), @@ -116,6 +117,13 @@ def get_beacon_noargs(): return get_beacon(radio, debug=verbose, logname=logname tasko.schedule(beacon_frequency_hz, get_beacon_noargs, 10) tasko.run() + elif choice in prompt_options["Image request loop"]: + image_period = get_input_range("Request period (seconds)", (120, 1000), allow_default=False) + image_frequency_hz = 1.0 / float(image_period) + def get_image_noargs(): return get_image(radio, debug=verbose) + tasko.schedule(image_frequency_hz, get_image_noargs, 10) + tasko.run() + elif choice in prompt_options["Upload file"]: source = input('source path = ') dest = input('destination path = ') diff --git a/gs_shell_tasks.py b/gs_shell_tasks.py index e383323..20277ec 100644 --- a/gs_shell_tasks.py +++ b/gs_shell_tasks.py @@ -61,3 +61,12 @@ async def get_beacon(radio, debug=False, logname=""): timestamped_log_print(bs, logname=logname) else: timestamped_log_print(f"Failed beacon request", printcolor=red, logname=logname) + + +async def get_image(radio, debug=False, logname=""): + timestamped_log_print("Requesting image...", logname=logname) + success = await request_image(radio, debug=debug) + if success: + timestamped_log_print("Successful image request", printcolor=green, logname=logname) + else: + timestamped_log_print("Failed image request", printcolor=red, logname=logname) \ No newline at end of file diff --git a/lib/radio_utils/commands.py b/lib/radio_utils/commands.py index 8a52b0d..21dfaeb 100644 --- a/lib/radio_utils/commands.py +++ b/lib/radio_utils/commands.py @@ -8,9 +8,11 @@ from pycubed import cubesat import radio_utils from radio_utils import transmission_queue as tq +from radio_utils import image_queue as iq from radio_utils import headers from radio_utils.disk_buffered_message import DiskBufferedMessage from radio_utils.memory_buffered_message import MemoryBufferedMessage +from radio_utils.image_message import ImageMessage from radio_utils.message import Message import json import supervisor @@ -36,6 +38,7 @@ GET_RTC_UTIME = b'\x00\x15' SET_RTC = b'\x00\x16' CLEAR_TX_QUEUE = b'\x00\x17' +REQUEST_IMAGE = b'\x00\x18' COMMAND_ERROR_PRIORITY = 9 BEACON_PRIORITY = 10 @@ -165,6 +168,17 @@ def request_beacon(task): """ _downlink_msg(beacon_packet(), header=headers.BEACON, priority=BEACON_PRIORITY, with_ack=False) +def request_image(task): + """Request a jpeg image + + :param task: The task that called this function + """ + # get filepath to image in image_queue + filepath = iq.peek() + # make image message from this + image = ImageMessage(filepath) + tq.push(image) + def get_rtc(task): """Get the RTC time""" _downlink_msg(_pack(tuple(cubesat.rtc.datetime))) @@ -268,6 +282,7 @@ def _unpack(data): SET_RTC: {"function": set_rtc, "name": "SET_RTC", "will_respond": False, "has_args": True}, SET_RTC_UTIME: {"function": set_rtc_utime, "name": "SET_RTC_UTIME", "will_respond": False, "has_args": True}, CLEAR_TX_QUEUE: {"function": clear_tx_queue, "name": "CLEAR_TX_QUEUE", "will_respond": False, "has_args": False}, + REQUEST_IMAGE: {"function": request_image, "name": "REQUEST_IMAGE", "will_respond": True, "has_args": False} } super_secret_code = b'p\xba\xb8C' diff --git a/lib/radio_utils/headers.py b/lib/radio_utils/headers.py index 5980cbd..61a245d 100644 --- a/lib/radio_utils/headers.py +++ b/lib/radio_utils/headers.py @@ -8,6 +8,10 @@ DISK_BUFFERED_MID = 0xfb DISK_BUFFERED_END = 0xfa +IMAGE_START = 0xEF +IMAGE_MID = 0xEE +IMAGE_END = 0xED + COMMAND = 0x01 BEACON = 0x02 diff --git a/lib/radio_utils/image_message.py b/lib/radio_utils/image_message.py new file mode 100644 index 0000000..8df7363 --- /dev/null +++ b/lib/radio_utils/image_message.py @@ -0,0 +1,131 @@ +from .message import Message +import os +from .headers import IMAGE_START, IMAGE_MID, IMAGE_END + +class ImageMessage(Message): + """ + encodes JPEG files into packets that can be transmitted over RF + - works for baseline DCT + + You can find out if your JPEG image uses baseline DCT by looking at the start of frame + bytes. If they are FFC0, it is baseline otherwise it will be FFC2 + """ + + headers = { + 0xFF, 0xD8, 0xC0, 0xC2, 0xC4, 0xDA, 0xDB, 0xDD, 0xFE, 0xD9 + } + + # SIG = bytearray.fromhex("FF") + SIG = bytearray(1) + SIG[0] = 0xFF + # SOI = bytearray.fromhex("D8") # Start of image + # SOFb = bytearray.fromhex("C0") # Start of frame (baseline DCT) + # SOFp = bytearray.fromhex("C2") # start of frame (progressive DCT) + # DHT = bytearray.fromhex("C4") # Define Huffman Tables + # SOS = bytearray.fromhex("FFDA") # Start of scan + SOS = bytearray(2) + SOS[0] = 0xFF + SOS[1] = 0xDA + # DQT = bytearray.fromhex("DB") # Define Quntization table + # DRI = bytearray.fromhex("DD") # Define Restart Interval + # RST = bytearray.fromhex("D") # Restart + # FLEX = bytearray.fromhex("E") # Variable + # CMT = bytearray.fromhex("FE") # Comment + # EOI = bytearray.fromhex("FFD9") # End of Image + EOI = bytearray(2) + EOI[0] = 0xFF + EOI[1] = 0xD9 + + def __init__(self, filepath, packet_size) -> None: + self.packet_size = packet_size + self.filepath = filepath + self.length = os.stat(filepath)[6] + self.sent_packet_len = 0 + self.cursor = 0 + self.in_scan = False + self.file_err = False + self.scan_size = ((self.packet_size - 1) // 64) * 64 + + def packet(self): + """ + Packetizes the image into packets of a specified size limit + Packet 1 + SOI and JFIF-APP0 + Packet 2 to packet i + comment + packet i + 1 to packet j + frame, Quntization and huffman tables + packet j + 1 to k + image scan + """ + next_packet_found = False + data_len = 0 + + try: + with open(self.filepath, "rb") as file: + file.seek(self.cursor) + data_bytes = file.read(self.packet_size - 1) + except Exception as e: + print(f"Error reading from image file: {e}") + self.file_err = True + + if self.in_scan: + """ + Should use 64 byte increments in the image scan section + """ + data_len = self.scan_size + packet = bytearray(self.scan_size + 1) + packet[1:] = data_bytes[0:data_len] + else: + """ + If we are stil in the header bytes + """ + if self.SOS in data_bytes[:2]: + """ + If SOS sends just the SOS bytes + """ + self.in_scan = True + next_packet_found = True + data_len = 2 + if self.sent_packet_len != 0: + next_packet_found = True + data_len = self.sent_packet_len - 1 + length = len(data_bytes) + bdr = bytearray(reversed(data_bytes)) + start = 1 + while not next_packet_found: + signal_index = bdr.find(self.SIG, 1, self.packet_size - 1) + if signal_index == -1: + """section is larger than packet size""" + data_len = self.packet_size - 1 + next_packet_found = True + if bdr[signal_index - 1] in self.headers: + data_len = length - signal_index - 1 + next_packet_found = True + else: + start += signal_index + + packet = bytearray(data_len + 1) + packet[1:] = data_bytes[0:data_len] + if self.cursor == 0: + """start packet""" + packet[0] = IMAGE_START + elif self.EOI in packet: + """end packet""" + packet[0] = IMAGE_END + else: + """mid packet""" + packet[0] = IMAGE_MID + self.sent_packet_len = data_len + 1 + + return packet, True + + def done(self): + return (self.length <= self.cursor) or self.file_err + + def ack(self): + """ + confirms that we should move to the next packet of info + """ + self.cursor += self.sent_packet_len - 1 + self.sent_packet_len = 0 \ No newline at end of file diff --git a/lib/radio_utils/image_queue.py b/lib/radio_utils/image_queue.py new file mode 100644 index 0000000..2ddab61 --- /dev/null +++ b/lib/radio_utils/image_queue.py @@ -0,0 +1,52 @@ +"""The Transmission Queue is a max heap of messages to be transmitted. + +Messages must support the `__lt__`, `__le__`, `__eq__`, `__ge__`, and `__gt__` operators. +This enables to the max heap to compare messages based on their priority. +""" +from .queue import Queue + +limit = 100 +image_queue = Queue(limit) + + +def enq(msg): + """Push a filepath on the image queue + + :param msg: The message to push + :type msg: string + """ + image_queue.enq(msg) + + +def peek(): + """Returns the next filepath to an image to be transmitted + + :return: The next filepath to be transmitted + :rtype: string + """ + return image_queue.peek() + + +def pop(): + """Returns the next filepath to be transmitted and removes it from the transmission queue + + :return: The next fielpath to be transmitted + :rtype: string + """ + return image_queue.deq() + + +def empty(): + """Returns if the transmission queue is empty""" + return image_queue.empty() + + +def clear(): + """Clears the transmission queue""" + global image_queue + image_queue = Queue(limit) + + +def size(): + """Returns the number of messages in the transmission queue""" + return image_queue.length \ No newline at end of file diff --git a/lib/radio_utils/queue.py b/lib/radio_utils/queue.py new file mode 100644 index 0000000..debd3a9 --- /dev/null +++ b/lib/radio_utils/queue.py @@ -0,0 +1,52 @@ +""" +Simple Queue library by Thomas Damiani + +Implemented via a linked list +""" + +class Node: + def __init__(self, element) -> None: + self.data = element + self.next = None + +class Queue: + """Queue class""" + def __init__(self, max_length): + """Create an empty list""" + self.head = Node(None) + self.tail = self.head + self.capacity = max_length + self.length = 0 + + def empty(self): + """Queue is empty when the head and tail point to the same Node""" + return self.tail is self.head + + def enq(self, element): + """ + Places a new node on the end of the list by making tail.next this new + node and then making the new node the tail + """ + if self.length + 1 > self.capacity: + return + elem = Node(element) + if not self.tail: + self.tail.next = elem + self.tail = elem + self.length += 1 + + def enq(self): + """ + If queue is not empty, takes the element at the head and returns it + while making head.next the new head. + """ + if self.empty(): + raise Exception("Queue Empty") + result = self.head.data + self.head = self.head.next + return result + + def peek(self): + if self.empty(): + raise Exception("Queue Empty") + return self.head.data \ No newline at end of file