kopia lustrzana https://github.com/projecthorus/wenet
Updates to PacketTX, new PiCam wrapper. Basic automatic image TX functionality working.
rodzic
8ea910bdb3
commit
28d5a5cb1f
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
220
tx/WenetPiCam.py
220
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()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Ładowanie…
Reference in New Issue