Added Tawhiri predictions. Added geo link for landing position

pull/23/head
Mark Jessop 2020-12-28 18:51:47 +10:30
rodzic fb39078412
commit e6c314109a
8 zmienionych plików z 308 dodań i 66 usunięć

Wyświetl plik

@ -25,7 +25,8 @@ default_config = {
'thunderforest_api_key': 'none', 'thunderforest_api_key': 'none',
# Predictor settings # Predictor settings
'pred_enabled': False, # Enable running and display of predicted flight paths. 'pred_enabled': True, # Enable running and display of predicted flight paths.
'offline_predictions': False, # Use an offline GFS model and predictor instead of Tawhiri.
# Default prediction settings (actual values will be used once the flight is underway) # Default prediction settings (actual values will be used once the flight is underway)
'pred_model': "Disabled", 'pred_model': "Disabled",
'pred_desc_rate': 6.0, 'pred_desc_rate': 6.0,
@ -87,6 +88,7 @@ def parse_config_file(filename):
# Predictor # Predictor
chase_config['pred_enabled'] = config.getboolean('predictor', 'predictor_enabled') chase_config['pred_enabled'] = config.getboolean('predictor', 'predictor_enabled')
chase_config['offline_predictions'] = config.getboolean('predictor', 'offline_predictions')
chase_config['pred_burst'] = config.getfloat('predictor', 'default_burst') chase_config['pred_burst'] = config.getfloat('predictor', 'default_burst')
chase_config['pred_desc_rate'] = config.getfloat('predictor', 'default_descent_rate') chase_config['pred_desc_rate'] = config.getfloat('predictor', 'default_descent_rate')
chase_config['pred_binary'] = config.get('predictor','pred_binary') chase_config['pred_binary'] = config.get('predictor','pred_binary')

Wyświetl plik

@ -0,0 +1,134 @@
#!/usr/bin/env python
#
# Project Horus - Browser-Based Chase Mapper - Tawhiri Interface
#
# Grab predictions from the Tawhiri Predictions API
# Refer here for documentation on Tawhiri: https://tawhiri.readthedocs.io/en/latest/api.html
#
# Copyright (C) 2020 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
import datetime
import logging
import pytz
import requests
import subprocess
from dateutil.parser import parse
from threading import Thread
TAWHIRI_API_URL = "http://predict.cusf.co.uk/api/v1/"
def get_tawhiri_prediction(
launch_datetime,
launch_latitude,
launch_longitude,
launch_altitude=0,
ascent_rate=5.0,
burst_altitude=30000.0,
descent_rate=5.0,
profile='standard_profile',
dataset=None,
timeout = 10
):
""" Request a Prediction from the Tawhiri Predictor API """
# Localise supplied time to UTC if not already done
if launch_datetime.tzinfo is None:
launch_datetime = pytz.utc.localize(launch_datetime)
# Create RFC3339-compliant timestamp
_dt_rfc3339 = launch_datetime.isoformat()
_params = {
"launch_latitude": launch_latitude,
"launch_longitude": launch_longitude,
"launch_altitude": launch_altitude,
"launch_datetime": _dt_rfc3339,
"ascent_rate": ascent_rate,
"descent_rate": descent_rate,
"burst_altitude": burst_altitude,
"profile": profile
}
if dataset:
_params["dataset"] = dataset
logging.debug("Tawhiri - Requesting prediction using parameters: %s" % str(_params))
try:
_r = requests.get(TAWHIRI_API_URL, params=_params, timeout=timeout)
_json = _r.json()
if 'error' in _json:
# The Tawhiri API has returned an error
_error = "%s: %s" % (_json['error']['type'], _json['error']['description'])
logging.error("Tawhiri - %s" % _error)
return None
else:
return parse_tawhiri_data(_json)
except Exception as e:
logging.error("Tawhiri - Error running prediction: %s" % str(e))
return None
def parse_tawhiri_data(data):
""" Parse a returned flight trajectory from Tawhiri, and convert it to a cusf_predictor_wrapper compatible format """
# Extract dataset information
_dataset = parse(data['request']['dataset'])
_dataset = _dataset.strftime("%Y%m%d%Hz")
_path = []
for _stage in data['prediction']:
_trajectory = _stage['trajectory']
for _point in _trajectory:
_dt = parse(_point['datetime']).timestamp()
_path.append([_dt, _point['latitude'], _point['longitude'], _point['altitude']])
_output = {
"dataset": _dataset,
"path": _path
}
return _output
if __name__ == "__main__":
import datetime
import pprint
logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.INFO)
_now = datetime.datetime.utcnow()
# Regular complete-flightpath prediction
_data = get_tawhiri_prediction(
launch_datetime=_now,
launch_latitude=-34.9499,
launch_longitude=138.5194,
launch_altitude=0,
)
pprint.pprint(_data)
# Descent prediction
_data = get_tawhiri_prediction(
launch_datetime=_now,
launch_latitude=-34.9499,
launch_longitude=138.5194,
launch_altitude=10000,
burst_altitude=10001,
descent_rate=abs(-6.0)
)
pprint.pprint(_data)

Wyświetl plik

@ -82,17 +82,28 @@ thunderforest_api_key = none
# Predictor Settings # Predictor Settings
# Use of the predictor requires installing the CUSF Predictor Python Wrapper from here: # By default this will attempt to get predictions from the online Tawhiri Predictions API.
# https://github.com/darksidelemm/cusf_predictor_wrapper # Optionally, you can enable offline predictions below.
# You also need to compile the predictor binary, and copy it into this directory.
[predictor] [predictor]
# Enable Predictor (True/False) - This can also be enabled from the web client. # Enable Predictor (True/False) - This can also be enabled from the web client.
predictor_enabled = False predictor_enabled = True
# Predictor defaults - these can be modified at runtime in the web interface. # Predictor defaults - these can be modified at runtime in the web interface.
default_burst = 30000 default_burst = 30000
default_descent_rate = 5.0 default_descent_rate = 5.0
# Offline Predictions
# Use of the offline predictor requires installing the CUSF Predictor Python Wrapper from here:
# https://github.com/darksidelemm/cusf_predictor_wrapper
# You also need to compile the predictor binary, and copy it into this directory.
#
# Note: This setting turns offline predictions *on* by default, which assumes there is a valid
# GFS dataset already present and available.
# If you will be using the 'Download Model' button, then leave this at False, and Offline predictions
# will be enabled once a valid model is available.
offline_predictions = False
# Predictory Binary Location # Predictory Binary Location
# Where to find the built CUSF predictor binary. This will usually be ./pred or pred.exe (on Windows) # Where to find the built CUSF predictor binary. This will usually be ./pred or pred.exe (on Windows)
pred_binary = ./pred pred_binary = ./pred

Wyświetl plik

@ -29,6 +29,7 @@ from chasemapper.predictor import predictor_spawn_download, model_download_runni
from chasemapper.habitat import HabitatChaseUploader, initListenerCallsign, uploadListenerPosition from chasemapper.habitat import HabitatChaseUploader, initListenerCallsign, uploadListenerPosition
from chasemapper.logger import ChaseLogger from chasemapper.logger import ChaseLogger
from chasemapper.bearings import Bearings from chasemapper.bearings import Bearings
from chasemapper.tawhiri import get_tawhiri_prediction
# Define Flask Application, and allow automatic reloading of templates for dev work # Define Flask Application, and allow automatic reloading of templates for dev work
@ -307,7 +308,10 @@ def run_prediction():
''' Run a Flight Path prediction ''' ''' Run a Flight Path prediction '''
global chasemapper_config, current_payloads, current_payload_tracks, predictor, predictor_semaphore global chasemapper_config, current_payloads, current_payload_tracks, predictor, predictor_semaphore
if (predictor == None) or (chasemapper_config['pred_enabled'] == False): if (chasemapper_config['pred_enabled'] == False):
return
if (chasemapper_config['offline_predictions'] == True) and (predictor == None):
return return
# Set the semaphore so we don't accidentally kill the predictor object while it's running. # Set the semaphore so we don't accidentally kill the predictor object while it's running.
@ -338,8 +342,39 @@ def run_prediction():
else: else:
_burst_alt = chasemapper_config['pred_burst'] _burst_alt = chasemapper_config['pred_burst']
logging.info("Running Predictor for: %s." % _payload)
_pred_path = predictor.predict( if predictor == "Tawhiri":
logging.info("Requesting Prediction from Tawhiri for %s." % _payload)
# Tawhiri requires that the burst altitude always be higher than the starting altitude.
if _current_pos['is_descending']:
_burst_alt = _current_pos['alt'] + 1
# Tawhiri requires that the ascent rate be > 0 for standard profiles.
if _current_pos['ascent_rate'] < 0.1:
_current_pos['ascent_rate'] = 0.1
_tawhiri = get_tawhiri_prediction(
launch_datetime=_current_pos['time'],
launch_latitude=_current_pos['lat'],
launch_longitude=_current_pos['lon'],
launch_altitude=_current_pos['alt'],
burst_altitude=_burst_alt,
ascent_rate=_current_pos['ascent_rate'],
descent_rate=_desc_rate,
)
if _tawhiri:
_pred_path = _tawhiri['path']
_dataset = _tawhiri['dataset'] + " (Online)"
# Inform the client of the dataset age
flask_emit_event('predictor_model_update',{'model':_dataset})
else:
_pred_path = []
else:
logging.info("Running Offline Predictor for %s." % _payload)
_pred_path = predictor.predict(
launch_lat=_current_pos['lat'], launch_lat=_current_pos['lat'],
launch_lon=_current_pos['lon'], launch_lon=_current_pos['lon'],
launch_alt=_current_pos['alt'], launch_alt=_current_pos['alt'],
@ -383,17 +418,42 @@ def run_prediction():
# Abort predictions # Abort predictions
if chasemapper_config['show_abort'] and (_current_pos['alt'] < chasemapper_config['pred_burst']) and (_current_pos['is_descending'] == False): if chasemapper_config['show_abort'] and (_current_pos['alt'] < chasemapper_config['pred_burst']) and (_current_pos['is_descending'] == False):
logging.info("Running Abort Predictor for: %s." % _payload)
_abort_pred_path = predictor.predict( if predictor == "Tawhiri":
launch_lat=_current_pos['lat'], logging.info("Requesting Abort Prediction from Tawhiri for %s." % _payload)
launch_lon=_current_pos['lon'],
launch_alt=_current_pos['alt'], # Tawhiri requires that the ascent rate be > 0 for standard profiles.
if _current_pos['ascent_rate'] < 0.1:
_current_pos['ascent_rate'] = 0.1
_tawhiri = get_tawhiri_prediction(
launch_datetime=_current_pos['time'],
launch_latitude=_current_pos['lat'],
launch_longitude=_current_pos['lon'],
launch_altitude=_current_pos['alt'],
burst_altitude=_burst_alt + 200,
ascent_rate=_current_pos['ascent_rate'], ascent_rate=_current_pos['ascent_rate'],
descent_rate=_desc_rate, descent_rate=_desc_rate,
burst_alt=_current_pos['alt']+200, )
launch_time=_current_pos['time'],
descent_mode=_current_pos['is_descending']) if _tawhiri:
_abort_pred_path = _tawhiri['path']
else:
_abort_pred_path = []
else:
logging.info("Running Offline Abort Predictor for: %s." % _payload)
_abort_pred_path = predictor.predict(
launch_lat=_current_pos['lat'],
launch_lon=_current_pos['lon'],
launch_alt=_current_pos['alt'],
ascent_rate=_current_pos['ascent_rate'],
descent_rate=_desc_rate,
burst_alt=_current_pos['alt']+200,
launch_time=_current_pos['time'],
descent_mode=_current_pos['is_descending'])
if len(_pred_path) > 1: if len(_pred_path) > 1:
# Valid Prediction! # Valid Prediction!
@ -439,56 +499,73 @@ def run_prediction():
def initPredictor(): def initPredictor():
global predictor, predictor_thread, chasemapper_config, pred_settings global predictor, predictor_thread, chasemapper_config, pred_settings
try:
from cusfpredict.predict import Predictor
from cusfpredict.utils import gfs_model_age, available_gfs
# Check if we have any GFS data
_model_age = gfs_model_age(pred_settings['gfs_path'])
if _model_age == "Unknown":
logging.error("No GFS data in directory.")
chasemapper_config['pred_model'] = "No GFS Data."
flask_emit_event('predictor_model_update',{'model':"No GFS data."})
chasemapper_config['pred_enabled'] = False
else:
# Check model contains data to at least 4 hours into the future.
(_model_start, _model_end) = available_gfs(pred_settings['gfs_path'])
_model_now = datetime.utcnow() + timedelta(0,60*60*4)
if (_model_now < _model_start) or (_model_now > _model_end):
# No suitable GFS data!
logging.error("GFS Data in directory does not cover now!")
chasemapper_config['pred_model'] = "Old GFS Data."
flask_emit_event('predictor_model_update',{'model':"Old GFS data."})
chasemapper_config['pred_enabled'] = False
if chasemapper_config['offline_predictions']:
# Attempt to initialize an Offline Predictor instance
try:
from cusfpredict.predict import Predictor
from cusfpredict.utils import gfs_model_age, available_gfs
# Check if we have any GFS data
_model_age = gfs_model_age(pred_settings['gfs_path'])
if _model_age == "Unknown":
logging.error("No GFS data in directory.")
chasemapper_config['pred_model'] = "No GFS Data."
flask_emit_event('predictor_model_update',{'model':"No GFS data."})
chasemapper_config['offline_predictions'] = False
else: else:
chasemapper_config['pred_model'] = _model_age # Check model contains data to at least 4 hours into the future.
flask_emit_event('predictor_model_update',{'model':_model_age}) (_model_start, _model_end) = available_gfs(pred_settings['gfs_path'])
predictor = Predictor(bin_path=pred_settings['pred_binary'], gfs_path=pred_settings['gfs_path']) _model_now = datetime.utcnow() + timedelta(0,60*60*4)
if (_model_now < _model_start) or (_model_now > _model_end):
# No suitable GFS data!
logging.error("GFS Data in directory does not cover now!")
chasemapper_config['pred_model'] = "Old GFS Data."
flask_emit_event('predictor_model_update',{'model':"Old GFS data."})
chasemapper_config['offline_predictions'] = False
# Start up the predictor thread if it is not running. else:
if predictor_thread == None: chasemapper_config['pred_model'] = _model_age + " (Offline)"
predictor_thread = Thread(target=predictorThread) flask_emit_event('predictor_model_update',{'model':_model_age + " (Offline)"})
predictor_thread.start() predictor = Predictor(bin_path=pred_settings['pred_binary'], gfs_path=pred_settings['gfs_path'])
# Set the predictor to enabled, and update the clients. # Start up the predictor thread if it is not running.
chasemapper_config['pred_enabled'] = True if predictor_thread == None:
predictor_thread = Thread(target=predictorThread)
predictor_thread.start()
# Set the predictor to enabled, and update the clients.
chasemapper_config['offline_predictions'] = True
except Exception as e:
traceback.print_exc()
logging.error("Loading predictor failed: " + str(e))
flask_emit_event('predictor_model_update',{'model':"Failed - Check Log."})
chasemapper_config['pred_model'] = "Failed - Check Log."
print("Loading Predictor failed.")
predictor = None
flask_emit_event('server_settings_update', chasemapper_config) else:
# No initialization required for the online predictor
predictor = "Tawhiri"
flask_emit_event('predictor_model_update',{'model':"Tawhiri"})
# Start up the predictor thread if it is not running.
if predictor_thread == None:
predictor_thread = Thread(target=predictorThread)
predictor_thread.start()
flask_emit_event('server_settings_update', chasemapper_config)
except Exception as e:
traceback.print_exc()
logging.error("Loading predictor failed: " + str(e))
flask_emit_event('predictor_model_update',{'model':"Failed - Check Log."})
chasemapper_config['pred_model'] = "Failed - Check Log."
print("Loading Predictor failed.")
predictor = None
def model_download_finished(result): def model_download_finished(result):
""" Callback for when the model download is finished """ """ Callback for when the model download is finished """
global chasemapper_config
if result == "OK": if result == "OK":
# Downloader reported OK, restart the predictor. # Downloader reported OK, restart the predictor.
chasemapper_config["offline_predictions"] = True
initPredictor() initPredictor()
else: else:
# Downloader reported an error, pass on to the client. # Downloader reported an error, pass on to the client.
@ -834,15 +911,17 @@ class WebHandler(logging.Handler):
def emit(self, record): def emit(self, record):
""" Emit a log message via SocketIO """ """ Emit a log message via SocketIO """
if 'socket.io' not in record.msg: # Deal with log records with no content.
# Convert log record into a dictionary if record.msg:
log_data = { if 'socket.io' not in record.msg:
'level': record.levelname, # Convert log record into a dictionary
'timestamp': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'), log_data = {
'msg': record.msg 'level': record.levelname,
} 'timestamp': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
# Emit to all socket.io clients 'msg': record.msg
socketio.emit('log_event', log_data, namespace='/chasemapper') }
# Emit to all socket.io clients
socketio.emit('log_event', log_data, namespace='/chasemapper')

Wyświetl plik

@ -56,6 +56,12 @@ html, body, #map {
width: 10em; width: 10em;
} }
.predictorModelValue {
display: inline-block;
margin-left: auto;
text-align: right;
}
.timeToLanding { .timeToLanding {
color:red; color:red;
font-weight: bold; font-weight: bold;

Wyświetl plik

@ -37,7 +37,7 @@ function serverSettingsUpdate(data){
// Accept a json blob of settings data from the client, and update our local store. // Accept a json blob of settings data from the client, and update our local store.
chase_config = data; chase_config = data;
// Update a few fields based on this data. // Update a few fields based on this data.
$("#predictorModel").html("<b>Current Model: </b>" + chase_config.pred_model); $("#predictorModelValue").text(chase_config.pred_model);
$('#burstAlt').val(chase_config.pred_burst.toFixed(0)); $('#burstAlt').val(chase_config.pred_burst.toFixed(0));
$('#descentRate').val(chase_config.pred_desc_rate.toFixed(1)); $('#descentRate').val(chase_config.pred_desc_rate.toFixed(1));
$('#predUpdateRate').val(chase_config.pred_update_rate.toFixed(0)); $('#predUpdateRate').val(chase_config.pred_update_rate.toFixed(0));

Wyświetl plik

@ -99,6 +99,13 @@ function telemetryTableDialog(e, row){
var _last_pos = balloon_positions[callsign].latest_data.position; var _last_pos = balloon_positions[callsign].latest_data.position;
$('#telemDialogPosition').html("<a href='geo:" + _last_pos[0].toFixed(5) + "," + _last_pos[1].toFixed(5) + "'>" + _last_pos[0].toFixed(5) + ", " + _last_pos[1].toFixed(5) + "</a>"); $('#telemDialogPosition').html("<a href='geo:" + _last_pos[0].toFixed(5) + "," + _last_pos[1].toFixed(5) + "'>" + _last_pos[0].toFixed(5) + ", " + _last_pos[1].toFixed(5) + "</a>");
if(balloon_positions[callsign].pred_marker != null){
var _pred_latlng = balloon_positions[callsign].pred_marker.getLatLng();
$('#telemDialogPredPosition').html("<a href='geo:" + _pred_latlng.lat.toFixed(5) + "," + _pred_latlng.lng.toFixed(5) + "'>" + _pred_latlng.lat.toFixed(5) + ", " + _pred_latlng.lng.toFixed(5) + "</a>");
}else{
$('#telemDialogPredPosition').html("Unknown");
}
var _buttons = { var _buttons = {
"Follow": function() { "Follow": function() {

Wyświetl plik

@ -451,7 +451,7 @@
// Predictor Functions // Predictor Functions
socket.on('predictor_model_update', function(data){ socket.on('predictor_model_update', function(data){
var _model_data = data.model; var _model_data = data.model;
$("#predictorModel").html("<b>Current Model: </b>" + _model_data); $("#predictorModelValue").text(_model_data);
}); });
socket.on('predictor_update', function(data){ socket.on('predictor_update', function(data){
@ -611,6 +611,9 @@
<div class="paramRow"> <div class="paramRow">
<b>Current Position:</b> <div style='float:right;' id='telemDialogPosition'></div><br/> <b>Current Position:</b> <div style='float:right;' id='telemDialogPosition'></div><br/>
</div> </div>
<div class="paramRow">
<b>Predicted Landing:</b> <div style='float:right;' id='telemDialogPredPosition'></div><br/>
</div>
<div class="paramRow"> <div class="paramRow">
<b>Select Action:</b><br/> <b>Select Action:</b><br/>
</div> </div>
@ -676,7 +679,7 @@
</hr> </hr>
<h3>Predictor</h3> <h3>Predictor</h3>
<div class="paramRow" id="predictorModel"> <div class="paramRow" id="predictorModel">
<b>Current Model: </b> Predictor Disabled <b>Current Model: </b> <div class="predictorModelValue" id="predictorModelValue">Disabled</div>
</div> </div>
<div class="paramRow"> <div class="paramRow">
<b>Download Model</b> <button type="button" class="paramSelector" id="downloadModel">Download</button> <b>Download Model</b> <button type="button" class="paramSelector" id="downloadModel">Download</button>