Numerous updates. New tx_picam script, GPS support.

pull/1/head
Mark Jessop 2016-12-10 22:04:10 +10:30
rodzic c029889ccf
commit 93111d9372
11 zmienionych plików z 193 dodań i 102 usunięć

Wyświetl plik

@ -110,7 +110,7 @@ Three windows should appear:
- The FSK Demodulator Modem Statistics window, and
- The SSDV Viewer GUI
You can kill all the RX processed by running the 'Kill RX' shortcut on the desktop.
You can kill all the RX processes by running the 'Kill RX' shortcut on the desktop.
If you have a recorded sample, you can replay it by doing:

Wyświetl plik

@ -1,11 +1,12 @@
# Wenet - The Swift One
Modulator and glue code for the 115kbps SSDV experiment.
The transmit side is designed to run on a Raspberry Pi, and the UART (/dev/ttyAMA0) is used to modulate a RFM22B in direct-asynchronous mode. I expect other transmitters could probably be used (i.e. NTX2's or similar) at lower bandwidths.
The transmit side is designed to run on a Raspberry Pi, and the UART (/dev/ttyAMA0) is used to modulate a RFM98W (yes, a LoRa module) in direct-asynchronous mode. I expect other transmitters could probably be used (i.e. NTX2's or similar) at lower bandwidths.
## Flight History
* v0.1 - First test flight on Horus 37, no FEC. Read more about that here: http://rfhead.net/?p=637
* v0.2 - Second test flight on Horus 39, with LDPC FEC enabled. Read more here: http://www.rowetel.com/?p=5344
* v0.3 - Third test flight - sometime in Jan 2017.
![Image downlinked via Wenet on Horus 38](http://rfhead.net/temp/wenet_fec_small.jpeg)
The above image was captured on Horus 38, and downlinked via Wenet.
@ -22,10 +23,11 @@ The above image was captured on Horus 38, and downlinked via Wenet.
* PyQtGraph & PyQt4 (for FSK Modem Stats and SSDV GUI: `pip install pyqtgraph`)
## Main Programs
* `rx_ssdv.py` - Reads in received packets (256 byte SSDV frames) via stdin, and decodes them to JPEGs. Also informs other processes (via UDP broadcast) of new data.
* `rx_gui.py` - Displays last received image, as commanded by rx_ssdv.py via UDP.
* `init_rfm22b.py` - Set RFM22B (attached via SPI to the RPi) into Direct-Asynchronous mode.
* `tx_picam.py` - Captures pictures using the PiCam, and transmits them.
* `rx/rx_ssdv.py` - Reads in received packets (256 byte SSDV frames) via stdin, and decodes them to JPEGs. Also informs other processes (via UDP broadcast) of new ssdv and telemetry data.
* `rx/rx_gui.py` - Displays last received image, as commanded by rx_ssdv.py via UDP.
* `tx/init_rfm22b.py` - Set RFM22B (attached via SPI to the RPi) into Direct-Asynchronous mode.
* `tx/init_rfm98w.py` - Set RFM98W (attached via SPI to the RPi) into Direct-Asynchronous mode.
* `tx_picam_gps.py` - Captures pictures using the PiCam, overlays GPS data and transmits them.
## Testing Scripts
* Run `python compress_test_images.py` from within ./test_images/ to produce the set of test ssdv-compressed files.
@ -42,10 +44,11 @@ The above image was captured on Horus 38, and downlinked via Wenet.
## Sending/Receiving Images
### TX Side
* The LDPC encoder library needs ldpc_enc.c compiled to a shared library. Run: `gcc -fPIC -shared -o ldpc_enc.so ldpc_enc.c` to do this.
* Run either `python tx_picam.py` (might need sudo to access camera & SPI) or `python tx_test_images.py` on the transmitter Raspberry Pi. There's also a start_tx.sh bash script which also sets up a RFM22B. I run this bash script from /etc/rc.local so it starts on boot.
* Run either `python WenetPiCam.py (might need sudo to access camera & SPI) or `python tx_test_images.py` on the transmitter Raspberry Pi. There's also a start_tx.sh bash script which also sets up a RFM22B or RFM98W. I run this bash script from /etc/rc.local so it starts on boot.
#### IMPORTANT NOTES
* While the transit code requests an output baud rate of 115200 baud from the RPi's UART, the acheived baud rate (due to clock divisors) is actually 115386.843 baud (measured using a frequency counter). All of the resampling within the receive chain had to be adjusted accordingly, which means CPU-intensive fractional decimators.
* While the transit code requests an output baud rate of 115200 baud from the RPi's UART, the acheived baud rate (due to clock divisors) on a RPi A+ is actually 115386.843 baud (measured using a frequency counter). All of the resampling within the receive chain had to be adjusted accordingly, which means CPU-intensive fractional decimators.
* Baud rates on other RPi models may be different - best to measure and check!
* Apparently the newer Raspberry Pi's (or possibly just a newer version of Raspbian) use the alternate UART hardware by default, which has a smaller transmit buffer. This may result in gaps between bytes, which will likely throw the rx timing estimation off.
### RX Side

Wyświetl plik

@ -15,9 +15,13 @@ TXFREQ=441.200
cd ~/wenet/tx/
#Uncomment to initialise a RFM22B
python init_rfm22b.py $TXFREQ
#python init_rfm22b.py $TXFREQ
# Uncomment for use with a RFM98W
#python init_rfm98w.py $TXFREQ
python init_rfm98w.py $TXFREQ
# Start the main TX Script.
python tx_picam.py $MYCALL &
# Note, this assumes there is a uBlox GPS available at /dev/ttyACM0
python tx_picam_gps.py $MYCALL &
# If you don't want any GPS overlays, you can comment the above line and run:
# python WenetPiCam.py $MYCALL &

Wyświetl plik

@ -132,7 +132,6 @@ class PacketTX(object):
if self.telemetry_queue.qsize()>0:
packet = self.telemetry_queue.get_nowait()
self.s.write(packet)
print("Sent Telemetry Packet.")
elif self.ssdv_queue.qsize()>0:
packet = self.ssdv_queue.get_nowait()
self.s.write(packet)

Wyświetl plik

@ -7,6 +7,7 @@
# WARNING: GPhoto is *really* slow at taking pictures.
# Suggest not using the multiple-image mode.
#
#
from time import sleep
from threading import Thread

Wyświetl plik

@ -10,6 +10,7 @@ from threading import Thread
import glob
import os
import datetime
import traceback
class WenetPiCam(object):
@ -75,7 +76,7 @@ class WenetPiCam(object):
def debug_message(self, message):
""" Write a debug message.
If debug_ptr was set to a function during init, this will
write pass the message to that function, else it will just print it.
pass the message to that function, else it will just print it.
This is used mainly to get updates on image capture into the Wenet downlink.
"""
@ -199,6 +200,7 @@ class WenetPiCam(object):
self.debug_message("Running Image Post-Processing")
post_process_ptr(capture_filename)
except:
traceback.print_exc()
self.debug_message("Image Post-Processing Failed.")
# SSDV'ify the image.

BIN
tx/areg_logo.png 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 30 KiB

Wyświetl plik

@ -13,6 +13,7 @@ import ctypes
from numpy.ctypeslib import ndpointer
import numpy as np
import time
import sys
# Attempt to load in ldpc_enc.so on startup.
@ -29,6 +30,7 @@ try:
except OSError as e:
print("WARNING: Could not find ldpc_enc.so! Have you compiled ldpc_enc.c? \n gcc -fPIC -shared -o ldpc_enc.so ldpc_enc.c")
sys.exit(1)
#
# LDPC Encoder.

Wyświetl plik

@ -1,73 +0,0 @@
#!/usr/bin/env python
#
# PiCam Transmitter Script
# Capture images from the PiCam, and transmit them.
#
# Mark Jessop <vk5qi@rfhead.net>
#
import PacketTX, sys, os, datetime
from PacketTX import write_debug_message
from wenet_util import *
try:
callsign = sys.argv[1]
if len(callsign)>6:
callsign = callsign[:6]
except:
print("Usage: python tx_picam.py CALLSIGN ")
sys.exit(1)
print("Using callsign: %s" % callsign)
tx_baud_rate = 115200 # Actually 115387, due to divider error.
fec = True
debug_output = False # If True, packet bits are saved to debug.bin as one char per bit.
def transmit_file(filename, tx_object):
file_size = os.path.getsize(filename)
if file_size % 256 > 0:
tx.set_idle_message("File size not a multiple of 256 bytes!")
return
print("Transmitting %d Packets." % (file_size/256))
f = open(filename,'rb')
for x in range(file_size/256):
data = f.read(256)
tx_object.tx_packet(data)
f.close()
print("Waiting for tx queue to empty...")
tx_object.wait()
tx = PacketTX.PacketTX(debug=debug_output, callsign=callsign, fec=fec, serial_baud = tx_baud_rate)
tx.start_tx()
image_id = 0
try:
while True:
# Capture image using PiCam
print("Capturing Image...")
capture_time = datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%SZ")
capture_multiple(filename="./tx_images/%s.jpg"%capture_time, debug_ptr = tx.set_idle_message)
#os.system("raspistill -t 100 -o ./tx_images/%s.jpg -vf -hf -w 1024 -h 768" % capture_time)
# Resize using convert
tx.set_idle_message("Converting Image to SSDV...")
#os.system("convert temp.jpg -resize %s\! temp.jpg" % tx_resolution)
# SSDV'ify the image.
os.system("ssdv -e -n -q 6 -c %s -i %d ./tx_images/%s.jpg temp.ssdv" % (callsign,image_id,capture_time))
# Transmit image
print("Transmitting...")
transmit_file("temp.ssdv",tx)
# Increment Counter
image_id = (image_id+1)%256
except KeyboardInterrupt:
print("Closing")
tx.close()

140
tx/tx_picam_gps.py 100644
Wyświetl plik

@ -0,0 +1,140 @@
#!/usr/bin/env python
#
# PiCam Transmitter Script - with GPS Data and Logo Overlay.
# Capture images from the PiCam, and transmit them.
#
# Mark Jessop <vk5qi@rfhead.net>
#
import PacketTX
import WenetPiCam
import ublox
import argparse
import time
import os
parser = argparse.ArgumentParser()
parser.add_argument("callsign", default="N0CALL", help="Payload Callsign")
parser.add_argument("--gps", default="/dev/ttyACM0", help="uBlox GPS Serial port. Defaults to /dev/ttyACM0")
parser.add_argument("--logo", default="none", help="Optional logo to overlay on image.")
parser.add_argument("--txport", default="/dev/ttyAMA0", type=str, help="Transmitter serial port. Defaults to /dev/ttyAMA0")
parser.add_argument("--baudrate", default=115200, type=int, help="Transmitter baud rate. Defaults to 115200 baud.")
args = parser.parse_args()
callsign = args.callsign
# Truncate callsign if it's too long.
if len(callsign) > 6:
callsign = callsign[:6]
print("Using Callsign: %s" % callsign)
# Start up Wenet TX.
tx = PacketTX.PacketTX(serial_port=args.txport, serial_baud=args.baudrate, callsign=callsign)
tx.start_tx()
# Sleep for a second to let the transmitter fire up.
time.sleep(1)
# Initialise a couple of global variables.
max_altitude = 0
# Try and start up the GPS rx thread.
try:
gps = ublox.UBloxGPS(port=args.gps,
dynamic_model = ublox.DYNAMIC_MODEL_AIRBORNE1G,
update_rate_ms = 500,
debug_ptr = tx.transmit_text_message)
except Exception as e:
tx.transmit_text_message("ERROR: Could not Open GPS - %s" % str(e), repeats=5)
gps = None
def poll_gps():
global gps, max_altitude
if gps != None:
gps_state = gps.read_state()
if gps_state['altitude'] > max_altitude:
max_altitude = gps_state['altitude']
# Define our post-processing callback function, which gets called by WenetPiCam
# after an image has been captured.
def post_process_image(filename):
""" Post-process the image, adding on Logo overlay and GPS data if requested. """
global gps, max_altitude, args, tx
# Try and grab current GPS data snapshot
if gps != None:
gps_state = gps.read_state()
# Send GPS telemetry packet here.
# Construct string which we will add onto the image.
if gps_state['numSV'] < 3:
# If we don't have enough sats for a lock, don't display any data.
# TODO: Use the GPS fix status values here instead.
gps_string = "No GPS Lock"
else:
gps_string = "Lat: %.5f Lon: %.5f Alt: %dm (%dm) Speed: H %03.1f kph V %02.1f m/s" % (gps_state['latitude'],
gps_state['longitude'],
int(gps_state['altitude']),
int(max_altitude),
gps_state['ground_speed'],
gps_state['ascent_rate'])
else:
gps_string = "No GPS"
# Build up our imagemagick 'convert' command line
overlay_str = "convert %s -gamma 0.8 -font Helvetica -pointsize 30 -gravity North " % filename
overlay_str += "-strokewidth 2 -stroke '#000C' -annotate +0+5 \"%s\" " % gps_string
overlay_str += "-stroke none -fill white -annotate +0+5 \"%s\" " % gps_string
# Add on logo overlay argument if we have been given one.
if args.logo != "none":
overlay_str += "%s -gravity SouthEast -composite " % args.logo
overlay_str += filename
tx.transmit_text_message("Adding overlays to image.")
os.system(overlay_str)
return
# Finally, initialise the PiCam capture object.
picam = WenetPiCam.WenetPiCam(resolution=(1920,1088), callsign=callsign, debug_ptr=tx.transmit_text_message, vertical_flip=True, horizontal_flip=True)
# .. and start it capturing continuously.
picam.run(destination_directory="./tx_images/",
tx = tx,
post_process_ptr = post_process_image
)
# Main 'loop'.
try:
while True:
poll_gps() # Keep polling the GPS so we keep track of our maximum altitude.
time.sleep(1)
except KeyboardInterrupt:
print("Closing")
picam.stop()
tx.close()
gps.close()

Wyświetl plik

@ -3,6 +3,7 @@
UBlox binary protocol handling
Copyright Andrew Tridgell, October 2012
https://github.com/tridge/pyUblox
Released under GNU GPL version 3 or later
Added UBloxGPS abstraction layer class for use with Wenet TX system.
@ -902,15 +903,16 @@ class UBloxGPS(object):
# Basic Position Information
'latitude': 0.0,
'longitude': 0.0,
'altitude': 0.0,
'ground_speed': 0.0,
'heading': 0.0,
'altitude': 0.0, # Altitude in metres.
'ground_speed': 0.0, # Ground speed in KPH
'ascent_rate': 0.0, # Descent rate in m/s
'heading': 0.0, # Heading in degrees True.
# GPS State
'gpsFix': 0,
'numSV': 0,
'iTOW': 0,
'dynamic_model': 0
'gpsFix': 0,
'numSV': 0, # Number of satellites in use.
'iTOW': 0, # Time-of-Week of last fix.
'dynamic_model': 0 # Current dynamic model in use.
}
state_writelock = False
state_readlock = False
@ -959,7 +961,7 @@ class UBloxGPS(object):
def debug_message(self, message):
""" Write a debug message.
If debug_ptr was set to a function during init, this will
write pass the message to that function, else it will just print it.
pass the message to that function, else it will just print it.
This is used mainly to get updates on image capture into the Wenet downlink.
"""
@ -999,17 +1001,27 @@ class UBloxGPS(object):
and update our internal state table.
"""
while self.rx_running:
msg = self.gps.receive_message()
if msg is None:
try:
msg = self.gps.receive_message()
msg_name = msg.name()
except Exception as e:
self.debug_message("WARNING: GPS Failure. Attempting to reconnect.")
self.write_state('numSV',0)
# Attempt to re-open GPS.
self.gps.close()
self.gps = UBlox(self.port, self.baudrate, self.timeout)
self.setup_ublox()
self.debug_message("WARNING: Reconnected to GPS unit")
continue
time.sleep(5)
try:
self.gps.close()
except:
pass
try:
self.gps = UBlox(self.port, self.baudrate, self.timeout)
self.setup_ublox()
self.debug_message("WARNING: GPS Re-connected.")
except:
continue
# If we have received a message we care about, unpack it and update our state dict.
if msg.name() == "NAV_SOL":
msg.unpack()
self.write_state('numSV', msg.numSV)
@ -1026,6 +1038,7 @@ class UBloxGPS(object):
msg.unpack()
self.write_state('ground_speed', msg.gSpeed*0.036) # Convert to kph
self.write_state('heading', msg.heading*1.0e-5)
self.write_state('ascent_rate', msg.velD/100.0)
elif msg.name() == "CFG_NAV5":
msg.unpack()