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
|
#!/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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
# 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()
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Ładowanie…
Reference in New Issue