Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions gs_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response is used within the send_command function.

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"""
Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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([])
8 changes: 8 additions & 0 deletions gs_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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 = ')
Expand Down
9 changes: 9 additions & 0 deletions gs_shell_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
15 changes: 15 additions & 0 deletions lib/radio_utils/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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'
4 changes: 4 additions & 0 deletions lib/radio_utils/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
131 changes: 131 additions & 0 deletions lib/radio_utils/image_message.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is SIG/SOS/EOI?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are specific bytes in the JPEG file format. All JPEG's start and end with the same bytes

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
52 changes: 52 additions & 0 deletions lib/radio_utils/image_queue.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you definitely just coppied this from the other queue, but this is probably a bad idea to add all this boiler plate just to make a slightly easier import.

At least on the pycubed-mini side we started having memory issues and stuff like this doesn't help.

Not at all your fault given this is how it's done else where. I'd either just make it the three lines needed ie:

from .queue import Queue
limit = 100
image_queue = Queue(limit)

Or leave a issue somewhere to do this later (so we don't forget).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Losha here, We could make this a much smaller file.


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
Loading