diff --git a/rx/rx_gui.py b/rx/rx_gui.py index 3826d62..0358012 100644 --- a/rx/rx_gui.py +++ b/rx/rx_gui.py @@ -1,6 +1,10 @@ #!/usr/bin/env python -#-*- coding:utf-8 -*- +# +# SSDV RX GUI +# +# +from WenetPackets import * import sip, socket, Queue from threading import Thread sip.setapi('QString', 2) @@ -63,7 +67,7 @@ def udp_rx(): s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) s.settimeout(1) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(('',7890)) + s.bind(('',WENET_IMAGE_UDP_PORT)) print("Started UDP Listener Thread.") udp_listener_running = True while udp_listener_running: diff --git a/rx/rx_ssdv.py b/rx/rx_ssdv.py index ac7a534..4ba246e 100644 --- a/rx/rx_ssdv.py +++ b/rx/rx_ssdv.py @@ -9,13 +9,20 @@ # -import os,sys, datetime, argparse, socket +import os +import sys +import datetime +import argparse +import socket +from WenetPackets import * parser = argparse.ArgumentParser() parser.add_argument("--hex", action="store_true", help="Take Hex strings as input instead of raw data.") parser.add_argument("--partialupdate",default=0,help="Push partial updates every N packets to GUI.") args = parser.parse_args() + + # Helper functions to extract data from SSDV packets. def ssdv_packet_info(packet): @@ -52,11 +59,34 @@ def ssdv_packet_string(packet_info): return "%s \tSSDV: %s, Img:%d, Pkt:%d, %dx%d" % (datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S.%fZ"), packet_info['packet_type'],packet_info['image_id'],packet_info['packet_id'],packet_info['width'],packet_info['height']) +# GUI updates are only sent locally. def trigger_gui_update(filename): gui_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) - gui_socket.sendto(filename,("127.0.0.1",7890)) + gui_socket.sendto(filename,("127.0.0.1",WENET_IMAGE_UDP_PORT)) gui_socket.close() +# Telemetry packets are send via UDP broadcast in case there is other software on the local +# network that wants them. +def broadcast_telemetry_packet(data): + telemetry_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + # Set up the telemetry socket so it can be re-used. + telemetry_socket.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) + telemetry_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + # We need the following if running on OSX. + try: + telemetry_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except: + pass + + # Send to broadcast if we can. + try: + telemetry_socket.sendto(json.dumps(data), ('', WENET_TELEMETRY_UDP_PORT)) + except socket.error: + telemetry_socket.sendto(json.dumps(data), ('127.0.0.1', WENET_TELEMETRY_UDP_PORT)) + + telemetry_socket.close() + # State variables current_image = -1 current_packet_count = 0 diff --git a/tx/PacketTX.py b/tx/PacketTX.py index 38fed12..1826ae5 100644 --- a/tx/PacketTX.py +++ b/tx/PacketTX.py @@ -102,7 +102,8 @@ class PacketTX(object): crc = struct.pack(" 254: + message = message[:254] + + packet = "\x00" + struct.pack("B",len(message)) + message + + self.queue_telemetry_packet(packet) + class BinaryDebug(object): diff --git a/tx/WenetPackets.py b/tx/WenetPackets.py new file mode 100644 index 0000000..ac805df --- /dev/null +++ b/tx/WenetPackets.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python2.7 +# +# Wenet Packet Generators / Decoders +# + +WENET_IMAGE_UDP_PORT = 7890 +WENET_TELEMETRY_UDP_PORT = 7891 + +# SSDV + +# Text Message + +# GPS/IMU Telemetry \ No newline at end of file diff --git a/tx/WenetPiCam.py b/tx/WenetPiCam.py index 2a8b9ce..b49c3aa 100644 --- a/tx/WenetPiCam.py +++ b/tx/WenetPiCam.py @@ -2,13 +2,14 @@ # # Wenet PiCam Wrapper Class. # -# +# PiCamera API: https://picamera.readthedocs.io/en/release-1.12/api_camera.html from picamera import PiCamera from time import sleep from threading import Thread import glob import os +import datetime class WenetPiCam(object): @@ -18,6 +19,7 @@ class WenetPiCam(object): Captures multiple images, picks the best, then transmits it via a PacketTX object. + """ def __init__(self,resolution=(1488,1120), @@ -25,11 +27,35 @@ class WenetPiCam(object): vertical_flip = False, horizontal_flip = False, temp_filename_prefix = 'picam_temp', - debug_ptr = None): + debug_ptr = None, + callsign = "N0CALL"): + + """ Instantiate a WenetPiCam Object + used to capture images from a PiCam using 'optimal' capture techniques. + + Keyword Arguments: + resolution: Tuple (x,y) containing desired image capture resolution. + NOTE: both x and y need to be multiples of 16 to be used with SSDV. + + num_images: Number of images to capture in sequence when the 'capture' function is called. + The 'best' (largest filesize) image is selected and saved. + + vertical_flip: Flip captured images vertically. + horizontal_flip: Flip captured images horizontally. + Used to correct for picam orientation. + + temp_filename_prefix: prefix used for temporary files. + + debug_ptr: 'pointer' to a function which can handle debug messages. + This function needs to be able to accept a string. + Used to get status messages into the downlink. + + """ self.debug_ptr = debug_ptr self.temp_filename_prefix = temp_filename_prefix self.num_images = num_images + self.callsign = callsign # Attempt to start picam. self.cam = PiCamera() @@ -62,7 +88,13 @@ class WenetPiCam(object): def close(self): self.cam.close() - def capture(self, filename='output.jpg'): + def capture(self, filename='picam.jpg'): + """ Capture an image using the PiCam + + Keyword Arguments: + filename: destination filename. + """ + # Attempt to capture a set of images. for i in range(self.num_images): self.debug_message("Capturing Image %d of %d" % (i+1,self.num_images)) @@ -92,4 +124,186 @@ class WenetPiCam(object): return True + def ssdvify(self, filename="output.jpg", image_id=0, quality=6): + """ Convert a supplied JPEG image to SSDV. + + Keyword Arguments: + filename: Source JPEG filename. + Output SSDV image will be saved to this filename, with .jpg replaced by .ssdv + callsign: Payload callsign. Max 6 Alphanumeric characters. + image_id: Image ID number. Must be incremented between images. + quality: JPEG quality level: 4 - 7, where 7 is 'lossless' (not recommended). + 6 provides good quality at decent file-sizes. + + """ + + # Get non-extension part of filename. + file_basename = filename[:-4] + + # Construct SSDV command-line. + ssdv_command = "ssdv -e -n -q %d -c %s -i %d %s %s.ssdv" % (quality, self.callsign, image_id, filename, file_basename) + print(ssdv_command) + # Update debug message. + self.debug_message("Converting image to SSDV.") + + # Run SSDV converter. + return_code = os.system(ssdv_command) + + if return_code != 0: + self.debug_message("ERROR: Could not perform SSDV Conversion.") + return "FAIL" + else: + return file_basename + ".ssdv" + + auto_capture_running = False + def auto_capture(self, destination_directory, transmit_packet, queue_empty, post_process_ptr=None, delay = 0, start_id = 0): + """ Automatically capture and transmit images in a loop. + Images are automatically saved to a supplied directory, with file-names + defined using a timestamp. + + Use the run() and stop() functions to start/stop this running. + + Keyword Arguments: + destination_directory: Folder to save images to. Both raw JPEG and SSDV images are saved here. + transmit_packet: A thread-safe python function which SSDV image packets will be pushed into for + transmission. + queue_empty: A function which reports the state of the transmission queue. We use this to decide when + to start pushing new packets into the queue. + post_process_ptr: An optional function which is called after the image is captured. This function + will be passed the path/filename of the captured image. + This can be used to add overlays, etc to the image before it is SSDVified and transmitted. + NOTE: This function need to modify the image in-place. + delay: An optional delay in seconds between capturing images. Defaults to 0. + This delay is added on top of any delays caused while waiting for the transmit queue to empty. + start_id: Starting image ID. Defaults to 0. + """ + + image_id = start_id + + while self.auto_capture_running: + # Sleep before capturing next image. + sleep(delay) + + # Grab current timestamp. + capture_time = datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%SZ") + capture_filename = destination_directory + "/%s_picam.jpg" % capture_time + + # Attempt to capture. + capture_successful = self.capture(capture_filename) + + # If capture was unsuccessful, exit out of this thead, as clearly + # the camera isn't working. + if not capture_successful: + return + + # Otherwise, proceed to post-processing step. + if post_process_ptr != None: + try: + self.debug_message("Running Image Post-Processing") + post_process_ptr(capture_filename) + except: + self.debug_message("Image Post-Processing Failed.") + + # SSDV'ify the image. + ssdv_filename = self.ssdvify(capture_filename, image_id=image_id) + + # Check the SSDV Conversion has completed properly. If not, break. + if ssdv_filename == "FAIL": + return + + + # Otherwise, read in the file and push into the TX buffer. + file_size = os.path.getsize(ssdv_filename) + + # Wait until the transmit queue is empty before pushing in packets. + self.debug_message("Waiting for SSDV TX queue to empty.") + while queue_empty() == False: + sleep(0.05) # Sleep for a short amount of time. + if self.auto_capture_running == False: + return + + # Inform ground station we are about to send an image. + self.debug_message("Transmitting %d PiCam SSDV Packets." % (file_size/256)) + + # Push SSDV file into transmit queue. + f = open(ssdv_filename,'rb') + for x in range(file_size/256): + data = f.read(256) + transmit_packet(data) + f.close() + + # Increment image ID. + image_id = (image_id + 1) % 256 + # Loop! + + + def run(self, destination_directory, transmit_packet, queue_empty, post_process_ptr=None, delay = 0, start_id = 0): + """ Start auto-capturing images in a thread. + + Refer auto_capture function above. + + Keyword Arguments: + destination_directory: Folder to save images to. Both raw JPEG and SSDV images are saved here. + transmit_packet: A thread-safe python function which SSDV image packets will be pushed into for + transmission. + queue_empty: A function which reports the state of the transmission queue. We use this to decide when + to start pushing new packets into the queue. + post_process_ptr: An optional function which is called after the image is captured. This function + will be passed the path/filename of the captured image. + This can be used to add overlays, etc to the image before it is SSDVified and transmitted. + NOTE: This function need to modify the image in-place. + delay: An optional delay in seconds between capturing images. Defaults to 0. + This delay is added on top of any delays caused while waiting for the transmit queue to empty. + start_id: Starting image ID. Defaults to 0. + """ + + self.auto_capture_running = True + + capture_thread = Thread(target=self.auto_capture, kwargs=dict( + destination_directory=destination_directory, + transmit_packet=transmit_packet, + queue_empty=queue_empty, + post_process_ptr=post_process_ptr, + delay=delay, + start_id=start_id)) + + capture_thread.start() + + def stop(self): + self.auto_capture_running = False + + +# Basic transmission test script. +if __name__ == "__main__": + import PacketTX + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("callsign", default="N0CALL", help="Payload Callsign") + args = parser.parse_args() + + callsign = args.callsign + print("Using Callsign: %s" % callsign) + + def post_process(filename): + print("Doing nothing with %s" % filename) + + tx = PacketTX.PacketTX(callsign=callsign) + tx.start_tx() + + picam = WenetPiCam(callsign=callsign, debug_ptr=None) + + picam.run(destination_directory="./tx_images/", + transmit_packet = tx.queue_image_packet, + queue_empty = tx.image_queue_empty, + post_process_ptr = post_process + ) + try: + while True: + tx.transmit_text_message("Waiting...") + sleep(5) + except KeyboardInterrupt: + print("Closing") + picam.stop() + tx.close() diff --git a/tx/tx_test_images.py b/tx/tx_test_images.py index 9fd1da0..a83f410 100644 --- a/tx/tx_test_images.py +++ b/tx/tx_test_images.py @@ -34,7 +34,7 @@ def transmit_file(filename, tx_object): tx_object.wait() -tx = PacketTX.PacketTX(debug=debug_output,fec=False) +tx = PacketTX.PacketTX(debug=debug_output) tx.start_tx() print("TX Started. Press Ctrl-C to stop.") try: