kopia lustrzana https://github.com/projecthorus/wenet
Numerous updates. New tx_picam script, GPS support.
rodzic
c029889ccf
commit
93111d9372
|
@ -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:
|
||||
|
|
17
README.md
17
README.md
|
@ -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.
|
||||
|
||||

|
||||
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
|
||||
|
|
10
start_tx.sh
10
start_tx.sh
|
@ -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 &
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 30 KiB |
|
@ -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.
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
45
tx/ublox.py
45
tx/ublox.py
|
@ -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()
|
||||
|
|
Ładowanie…
Reference in New Issue