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
#-*- 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:

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.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), ('<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
current_image = -1
current_packet_count = 0

Wyświetl plik

@ -102,7 +102,8 @@ class PacketTX(object):
crc = struct.pack("<H",self.crc16(packet))
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:
return self.preamble + self.unique_word + packet + crc
@ -154,6 +155,27 @@ class PacketTX(object):
while not self.ssdv_queue.empty():
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):

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.
#
#
# 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()

Wyświetl plik

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