Updates to web interface, added headless RX script.

pull/2/head
Mark Jessop 2019-07-28 17:20:40 +09:30
rodzic 0be700901d
commit f0f5af4281
12 zmienionych plików z 419 dodań i 72 usunięć

Wyświetl plik

@ -318,7 +318,7 @@ def orientation_telemetry_decoder(packet):
# We need the packet as a string - convert to a string in case we were passed a list of bytes,
# which occurs when we are decoding a packet that has arrived via a UDP-broadcast JSON blob.
packet = byts(bytearray(packet))
packet = bytes(bytearray(packet))
# Some basic sanity checking of the packet before we attempt to decode.
if len(packet) < WENET_PACKET_LENGTHS.ORIENTATION_TELEMETRY:

Wyświetl plik

@ -29,6 +29,7 @@ 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.")
parser.add_argument("-v", "--verbose", action='store_true', default=False, help="Verbose output")
parser.add_argument("--headless", action='store_true', default=False, help="Headless mode - broadcasts additional data via UDP.")
args = parser.parse_args()
@ -55,7 +56,7 @@ def trigger_gui_update(filename, text = "None"):
# 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):
def broadcast_telemetry_packet(data, headless=False):
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)
@ -79,6 +80,13 @@ def broadcast_telemetry_packet(data):
telemetry_socket.close()
if headless:
# In headless mode, we also send the above data via the image port.
gui_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
gui_socket.sendto(json.dumps(data).encode('ascii'),("127.0.0.1",WENET_IMAGE_UDP_PORT))
gui_socket.close()
# State variables
current_image = -1
current_callsign = ""
@ -113,7 +121,7 @@ while True:
if packet_type == WENET_PACKET_TYPES.IDLE:
continue
elif packet_type == WENET_PACKET_TYPES.TEXT_MESSAGE:
broadcast_telemetry_packet(data)
broadcast_telemetry_packet(data, args.headless)
logging.info(packet_to_string(data))
elif packet_type == WENET_PACKET_TYPES.SEC_PAYLOAD_TELEMETRY:
@ -121,7 +129,7 @@ while True:
logging.info(packet_to_string(data))
elif packet_type == WENET_PACKET_TYPES.GPS_TELEMETRY:
broadcast_telemetry_packet(data)
broadcast_telemetry_packet(data, args.headless)
logging.info(packet_to_string(data))
elif packet_type == WENET_PACKET_TYPES.ORIENTATION_TELEMETRY:

Wyświetl plik

@ -30,7 +30,7 @@ except ImportError:
# Python 3
from queue import Queue
WENET_IMAGE_UDP_PORT = 7890
from WenetPackets import WENET_IMAGE_UDP_PORT
class SSDVUploader(object):

Wyświetl plik

@ -0,0 +1,85 @@
/*
* Globals
*/
/* Links */
a,
a:focus,
a:hover {
color: #fff;
}
/*
* Base structure
*/
html,
body {
height: 100%;
background-color: #333;
}
body {
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-pack: center;
-webkit-box-pack: center;
justify-content: center;
color: #fff;
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
}
.cover-container {
max-width: 42em;
}
/*
* Header
*/
.masthead-brand {
margin-bottom: 1;
}
.wenet-image {
max-width: 100%;
max-height: auto;
}
.snr-display {
padding: .25rem 0;
font-weight: 700;
color: rgba(255, 255, 255, .8);
background-color: transparent;
float: right;
}
.gps-display {
padding: .25rem 0;
font-weight: 600;
color: rgba(255, 255, 255, .8);
background-color: transparent;
float: right;
}
@media (min-width: 48em) {
.masthead-brand {
float: left;
}
.nav-masthead {
float: right;
}
}
/*
* Footer
*/
.mastfoot {
color: rgba(255, 255, 255, .5);
}

BIN
rx/static/horus.png 100644

Plik binarny nie jest wyświetlany.

Po

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

Wyświetl plik

@ -0,0 +1,123 @@
<!DOCTYPE HTML>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=yes">
<title>Wenet Web Interface</title>
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/wenet.css') }}" rel="stylesheet">
<script src="{{ url_for('static', filename='js/jquery-3.3.1.min.js')}}"></script>
<script src="{{ url_for('static', filename='js/socket.io-1.4.5.js') }}"></script>
<script type="text/javascript" charset="utf-8">
$(document).ready(function() {
// Use the 'update_status' namespace for all of our traffic
namespace = '/update_status';
// Connect to the Socket.IO server.
// The connection URL has the following format:
// http[s]://<domain>:<port>[/<namespace>]
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
// Handle an image update.
socket.on('image_update', function(msg) {
var myImageElement = document.getElementById('wenet_image');
myImageElement.src = 'latest.jpg?rand=' + Math.random();
var _new_desc = msg.text;
$('#image_data').html(_new_desc);
});
socket.on('uploader_update', function(msg) {
var _new_desc = "Uploader Status: " + msg.queued + " Queued, " + msg.uploaded + " Uploaded, " + msg.discarded + " Discarded";
$('#uploader_data').html(_new_desc);
});
socket.on('modem_stats', function(msg) {
var snr = msg.snr.toFixed(1);
var ppm = msg.ppm.toFixed(1);
var _new_desc = "SNR: " + snr + " dB"
$('#snr-data').html(_new_desc);
});
socket.on('gps_update', function(msg) {
if (msg.numSV < 3){
$('#gps-data').html("No GPS Lock");
} else {
var lat = msg.latitude.toFixed(5);
var lon = msg.longitude.toFixed(5);
var alt = msg.altitude.toFixed(0);
var ascent = msg.ascent_rate.toFixed(1);
var _new_desc = lat + ", " + lon + " " + alt + "m " + ascent + " m/s"
$('#gps-data').html(_new_desc);
}
});
var text_messages = [];
socket.on('text_update', function(msg) {
var _text = "Msg #" + msg.id + ": " + msg.text;
text_messages.push(_text);
if(text_messages.length > 6){
text_messages.shift();
}
var _log_output = "";
text_messages.forEach( function(value, index, array){
_log_output = _log_output + value + "<br>";
});
$('#log_data').html(_log_output);
});
// Tell the server we are connected and ready for data.
socket.on('connect', function() {
socket.emit('client_connected', {data: 'I\'m connected!'});
});
});
</script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-4">
<h3 class="masthead-brand">Wenet Dashboard</h3>
</div>
<div class="col-6">
<h5 class="gps-display" id="gps-data"></h5>
</div>
<div class="col-2">
<h4 class="snr-display" id="snr-data">SNR: 0 dB</h4>
</div>
</div>
<div class="row">
<div class="col-12">
<img src="{{ url_for('static', filename='horus.png') }}" id="wenet_image" class="center-block wenet-image"/>
</div>
</div>
<div class="row">
<div class='col-6'>
<div id="image_data">No image data received yet.</div>
</div>
<div class='col-6'>
<div id="uploader_data">No uploader status data received yet.</div>
</div>
</div>
<div class="row">
<div class='col-12'>
<div id="log_data">No log messages received yet.</div>
</div>
</div>
</div>
</body>
</html>

Wyświetl plik

@ -1,65 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Wenet Web Interface</title>
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<script src="{{ url_for('static', filename='js/jquery-3.3.1.min.js')}}"></script>
<script src="{{ url_for('static', filename='js/socket.io-1.4.5.js') }}"></script>
<script type="text/javascript" charset="utf-8">
$(document).ready(function() {
// Use the 'update_status' namespace for all of our traffic
namespace = '/update_status';
// Connect to the Socket.IO server.
// The connection URL has the following format:
// http[s]://<domain>:<port>[/<namespace>]
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
// Handle an image update.
socket.on('image_update', function(msg) {
var myImageElement = document.getElementById('wenet_image');
myImageElement.src = 'latest.jpg?rand=' + Math.random();
var _new_desc = msg.text;
$('#image_data').html(_new_desc);
});
socket.on('uploader_update', function(msg) {
var _new_desc = "Uploader Status: " + msg.queued + " Queued, " + msg.uploaded + " Uploaded, " + msg.discarded + " Discarded";
$('#uploader_data').html(_new_desc);
});
// Tell the server we are connected and ready for data.
socket.on('connect', function() {
socket.emit('client_connected', {data: 'I\'m connected!'});
});
});
</script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<img src="latest.jpg" id="wenet_image" class="center-block img-responsive"/>
</div>
</div>
<div class="row">
<div class='col-12'>
<div id="image_data">No image data received yet.</div>
</div>
</div>
<div class="row">
<div class='col-12'>
<div id="uploader_data">No uploader status data received yet.</div>
</div>
</div>
</div>
</body>
</html>

Wyświetl plik

@ -25,7 +25,7 @@ import datetime
from threading import Thread, Lock
from io import BytesIO
WENET_IMAGE_UDP_PORT = 7890
from WenetPackets import *
# Define Flask Application, and allow automatic reloading of templates for dev
app = flask.Flask(__name__)
@ -102,6 +102,27 @@ def update_image(filename, description):
logging.error("Error loading new image %s - %s" % (filename, str(e)))
def handle_telemetry(packet):
""" Handle GPS and Text message packets from the wenet receiver """
# Decode GPS and IMU packets, and pass onto their respective GUI update functions.
packet_type = decode_packet_type(packet)
if packet_type == WENET_PACKET_TYPES.GPS_TELEMETRY:
# GPS data from the payload
gps_data = gps_telemetry_decoder(packet)
if gps_data['error'] == 'None':
flask_emit_event('gps_update', data=gps_data)
elif packet_type == WENET_PACKET_TYPES.TEXT_MESSAGE:
# A text message from the payload.
text_data = decode_text_message(packet)
if text_data['error'] == 'None':
flask_emit_event('text_update', data=text_data)
else:
# Discard any other packet type.
pass
def process_udp(packet):
@ -115,6 +136,16 @@ def process_udp(packet):
# Information from the uploader process.
flask_emit_event('uploader_update', data=packet_dict)
elif 'snr' in packet_dict:
# Modem statistics packet
flask_emit_event('modem_stats', data=packet_dict)
elif 'type' in packet_dict:
# Generic telemetry packet from the wenet RX.
# This could be GPS telemetry, text data, or something else..
if packet_dict['type'] == 'WENET':
handle_telemetry(packet_dict['packet'])
udp_listener_running = False
@ -154,10 +185,16 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-l", "--listen_port",default=5003,help="Port to run Web Server on. (Default: 5003)")
parser.add_argument("-v", "--verbose", action='store_true', help="Enable debug output.")
args = parser.parse_args()
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.DEBUG)
if args.verbose:
log_level = logging.DEBUG
else:
log_level = logging.ERROR
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=log_level)
t = Thread(target=udp_rx_thread)

Wyświetl plik

@ -0,0 +1,159 @@
#!/bin/bash
#
# Wenet RX-side Initialisation Script - HEADLESS VERSION
# 2019 Mark Jessop <vk5qi@rfhead.net>
#
# This code mostly assumes an RTLSDR will be used for RX.
# For the lower rate variants (4800/9600), GQRX could be used.
#
# This version of the startup script is intended to be run on a headless
# Raspberry Pi 3B+ or newer.
# A display of imagery and telemetry can be accessed at http://<pi_ip>:5003/
#
# Set CHANGEME to your callsign.
MYCALL=CHANGEME
# Wenet Transmission Centre Frequency:
# Default Wenet Frequency, as used on most Project Horus flights.
RXFREQ=441200000
# Secondary downlink frequency, used on dual-launch flights
#RXFREQ=443500000
# Receiver Gain. Set this to 0 to use automatic gain control, otherwise if running a
# preamplifier, you may want to experiment with different gain settings to optimize
# your receiver setup.
# You can find what gain range is valid for your RTLSDR by running: rtl_test
GAIN=0
# Bias Tee Enable (1) or Disable (0)
BIAS=0
# Note that this will need the rtl_biast utility available, which means
# building the rtl-sdr utils from this repo: https://github.com/rtlsdrblog/rtl-sdr
# Change the following path as appropriate.
# If running this from a .desktop file, you may need to set an absolute path here
# i.e. /home/username/wenet/rx/
cd ~/wenet/rx/
# Receive Flow Type:
# IQ = Pass complex samples into the fsk demodulator. (Default)
# This is suitable for use with RTLSDRs that do not have DC bias issues.
# Examples: RTLSDR-Blog v3 Dongles, most Nooelec dongles. (anything with a R820T or R820T2 tuner)
#
# SSB = Demodulate the IQ from the SDR as a very wide (400 kHz) USB signal, and
# pass that into the fsk demodulator.
# This is useful when the RTLSDR has a DC bias that may affect demodulation.
# i.e. RTLSDRs with Elonics E4000 or FitiPower FC0013 tuners.
# Note: This requires that the csdr utility be installed: https://github.com/simonyiszk/csdr.git
#
# GQRX = Take USB audio from GQRX, via a UDP stream on port 7355.
# This assumes that GQRX has been set into 'wide' (24 kHz BW) USB mode, and is
# streaming samples to UDP:localhost:7355.
# Note 1: This mode will only work for low baud rates (~4800-9600 baud),
# that can fit within a ~20 kHz passband. The baud rate must also be an integer divisor of 48 khz.
# Note 2: When in this mode, all the frequency/gain/bias commands above will be ignored, as GQRX
# has control over the SDR.
RX_FLOW=IQ
#
# Modem Settings - Don't adjust these unless you really need to!
#
BAUD_RATE=115177 # Baud rate, in symbols/second.
OVERSAMPLING=8 # FSK Demod Oversampling rate. Not used in GQRX mode.
# Known-Working Modem Settings:
# 115177 baud (Pi Zero W @ '115200' baud), 8x oversampling.
# 9600 baud, 100x oversampling.
# 4800 baud, 200x oversampling.
#BAUD_RATE=4800
#OVERSAMPLING=200
#
# Main Script Start... Don't edit anything below this unless you know what you're doing!
#
# Do some checks if we are in GQRX mode.
if [ "$RX_FLOW" = "GQRX" ]; then
if (($BAUD_RATE > 10000)); then
echo "Baud rate too high for GQRX mode."
exit 1
fi
fi
# Start up the SSDV Uploader script and push it into the background.
python ssdvuploader.py $MYCALL &
SSDV_UPLOAD_PID=$!
# Start the Web Interface Server
python wenetserver.py &
WEB_VIEWER_PID=$!
# Do some checks if we are in GQRX mode.
if [ "$RX_FLOW" != "GQRX" ]; then
# Calculate the SDR sample rate required.
SDR_RATE=$(($BAUD_RATE * $OVERSAMPLING))
# Calculate the SDR centre frequency.
# The fsk_demod acquisition window is from Rs/2 to Fs/2 - Rs.
# Given Fs is Rs * Os (Os = oversampling), we can calculate the required tuning offset with the equation:
# Offset = Fcenter - Rs*(Os/4 - 0.25)
RX_SSB_FREQ=$(echo "$RXFREQ - $BAUD_RATE * ($OVERSAMPLING/4 - 0.25)" | bc)
echo "Using SDR Sample Rate: $SDR_RATE Hz"
echo "Using SDR Centre Frequency: $RX_SSB_FREQ Hz"
if [ "$BIAS" = "1" ]; then
echo "Enabling Bias Tee"
rtl_biast -b 1
fi
fi
# Start up the receive chain.
if [ "$RX_FLOW" = "IQ" ]; then
# If we have a RTLSDR that receives using a low-IF, then we have no DC spike issues,
# and can feed complex samples straight into the fsk demodulator.
echo "Using Complex Samples."
rtl_sdr -s $SDR_RATE -f $RX_SSB_FREQ -g $GAIN - | \
./fsk_demod --cu8 -s --stats=100 2 $SDR_RATE $BAUD_RATE - - 2> >(python fskstatsudp.py --rate 1) | \
./drs232_ldpc - - -vv 2> /dev/null | \
python rx_ssdv.py --partialupdate 16 --headless
elif [ "$RX_FLOW" = "GQRX" ]; then
# GQRX Mode - take 48kHz real samples from GQRX via UDP.
# TODO: Check the following netcat command works OK under all OSes...
# different netcat versions seem to have different command-line options.
# Might need to try: nc -l -u -p 7355 localhost
echo "Receiving samples from GQRX on UDP:localhost:7355"
nc -l -u localhost 7355 | \
./fsk_demod -s --stats=100 -b 1 -u 23500 2 48000 $BAUD_RATE - - 2> >(python fskstatsudp.py --rate 1) | \
./drs232_ldpc - - -vv 2> /dev/null | \
python rx_ssdv.py --partialupdate 4 --headless
else
# If using a RTLSDR that has a DC spike (i.e. either has a FitiPower FC0012 or Elonics E4000 Tuner),
# we receive below the centre frequency, and perform USB demodulation.
echo "Using Real Samples and USB demodulation."
rtl_sdr -s $SDR_RATE -f $RX_SSB_FREQ -g $GAIN - | csdr convert_u8_f | \
csdr bandpass_fir_fft_cc 0.05 0.45 0.05 | csdr realpart_cf | \
csdr gain_ff 0.5 | csdr convert_f_s16 | \
./fsk_demod -s --stats=100 2 $SDR_RATE $BAUD_RATE - - 2> >(python fskstatsudp.py --rate 1) | \
./drs232_ldpc - - -vv 2> /dev/null | \
python rx_ssdv.py --partialupdate 16 --headless
fi
# Kill off the SSDV Uploader and the GUIs
kill $SSDV_UPLOAD_PID
kill $WEB_VIEWER_PID