Updates to PacketTX, new PiCam wrapper. Basic automatic image TX functionality working.

pull/1/head
Mark Jessop 2016-12-07 22:06:51 +10:30
rodzic 8ea910bdb3
commit 28d5a5cb1f
6 zmienionych plików z 292 dodań i 9 usunięć

Wyświetl plik

@ -1,6 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
#-*- coding:utf-8 -*- #
# SSDV RX GUI
#
#
from WenetPackets import *
import sip, socket, Queue import sip, socket, Queue
from threading import Thread from threading import Thread
sip.setapi('QString', 2) sip.setapi('QString', 2)
@ -63,7 +67,7 @@ def udp_rx():
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.settimeout(1) s.settimeout(1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('',7890)) s.bind(('',WENET_IMAGE_UDP_PORT))
print("Started UDP Listener Thread.") print("Started UDP Listener Thread.")
udp_listener_running = True udp_listener_running = True
while udp_listener_running: while udp_listener_running:

Wyświetl plik

@ -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 = argparse.ArgumentParser()
parser.add_argument("--hex", action="store_true", help="Take Hex strings as input instead of raw data.") 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.") parser.add_argument("--partialupdate",default=0,help="Push partial updates every N packets to GUI.")
args = parser.parse_args() args = parser.parse_args()
# Helper functions to extract data from SSDV packets. # Helper functions to extract data from SSDV packets.
def ssdv_packet_info(packet): 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']) 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): def trigger_gui_update(filename):
gui_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 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() 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), ('<broadcast>', 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 # State variables
current_image = -1 current_image = -1
current_packet_count = 0 current_packet_count = 0

Wyświetl plik

@ -102,7 +102,8 @@ class PacketTX(object):
crc = struct.pack("<H",self.crc16(packet)) crc = struct.pack("<H",self.crc16(packet))
if fec: if fec:
return self.preamble + self.unique_word + packet + crc + ldpc_encode_string(packet+crc) parity = ldpc_encode_string(packet + crc)
return self.preamble + self.unique_word + packet + crc + parity
else: else:
return self.preamble + self.unique_word + packet + crc return self.preamble + self.unique_word + packet + crc
@ -154,6 +155,27 @@ class PacketTX(object):
while not self.ssdv_queue.empty(): while not self.ssdv_queue.empty():
sleep(0.01) sleep(0.01)
def queue_image_packet(self,packet):
self.ssdv_queue.put(self.frame_packet(packet, self.fec))
def image_queue_empty(self):
return self.ssdv_queue.qsize() == 0
def queue_telemetry_packet(self, packet):
self.telemetry_queue.put(self.frame_packet(packet, self.fec))
def telemetry_queue_empty(self):
return self.telemetry_queue.qsize() == 0
def transmit_text_message(self,message):
# Clip message if required.
if len(message) > 254:
message = message[:254]
packet = "\x00" + struct.pack("B",len(message)) + message
self.queue_telemetry_packet(packet)
class BinaryDebug(object): class BinaryDebug(object):

13
tx/WenetPackets.py 100644
Wyświetl plik

@ -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

Wyświetl plik

@ -2,13 +2,14 @@
# #
# Wenet PiCam Wrapper Class. # Wenet PiCam Wrapper Class.
# #
# # PiCamera API: https://picamera.readthedocs.io/en/release-1.12/api_camera.html
from picamera import PiCamera from picamera import PiCamera
from time import sleep from time import sleep
from threading import Thread from threading import Thread
import glob import glob
import os import os
import datetime
class WenetPiCam(object): class WenetPiCam(object):
@ -18,6 +19,7 @@ class WenetPiCam(object):
Captures multiple images, picks the best, then Captures multiple images, picks the best, then
transmits it via a PacketTX object. transmits it via a PacketTX object.
""" """
def __init__(self,resolution=(1488,1120), def __init__(self,resolution=(1488,1120),
@ -25,11 +27,35 @@ class WenetPiCam(object):
vertical_flip = False, vertical_flip = False,
horizontal_flip = False, horizontal_flip = False,
temp_filename_prefix = 'picam_temp', 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.debug_ptr = debug_ptr
self.temp_filename_prefix = temp_filename_prefix self.temp_filename_prefix = temp_filename_prefix
self.num_images = num_images self.num_images = num_images
self.callsign = callsign
# Attempt to start picam. # Attempt to start picam.
self.cam = PiCamera() self.cam = PiCamera()
@ -62,7 +88,13 @@ class WenetPiCam(object):
def close(self): def close(self):
self.cam.close() 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. # Attempt to capture a set of images.
for i in range(self.num_images): for i in range(self.num_images):
self.debug_message("Capturing Image %d of %d" % (i+1,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 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()

Wyświetl plik

@ -34,7 +34,7 @@ def transmit_file(filename, tx_object):
tx_object.wait() tx_object.wait()
tx = PacketTX.PacketTX(debug=debug_output,fec=False) tx = PacketTX.PacketTX(debug=debug_output)
tx.start_tx() tx.start_tx()
print("TX Started. Press Ctrl-C to stop.") print("TX Started. Press Ctrl-C to stop.")
try: try: