Add data flush option. Add experimental offline map layer.

bearings
Mark Jessop 2018-07-22 22:03:55 +09:30
rodzic 5710303798
commit 9627bbb436
3 zmienionych plików z 218 dodań i 10 usunięć

Wyświetl plik

@ -5,3 +5,74 @@
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
import logging
import subprocess
from threading import Thread
model_download_running = False
def predictor_download_model(command, callback):
""" Run the supplied command, which should download a GFS model and place it into the GFS directory
When the downloader completes, or if an error is thrown, the status is passed to a callback function.
"""
global model_download_running
if model_download_running:
return
model_download_running = True
try:
ret_code = subprocess.call(command, shell=True)
except Exception as e:
# Something broke when running the detection function.
logging.error("Error when attempting to download model - %s" % (str(e)))
model_download_running = False
callback("Error - See log.")
return
model_download_running = False
if ret_code == 0:
logging.info("Model Download Completed.")
callback("OK")
return
else:
logging.error("Model Downloader returned code %d" % ret_code)
callback("Error: Ret Code %d" % ret_code)
return
def predictor_spawn_download(command, callback=None):
""" Spawn a model downloader in a new thread """
global model_download_running
if model_download_running:
return "Already Downloading."
_download_thread = Thread(target=predictor_download_model, kwargs={'command':command, 'callback': callback})
_download_thread.start()
return "Started downloader."
if __name__ == "__main__":
import sys
from .config import parse_config_file
from cusfpredict.utils import gfs_model_age, available_gfs
_cfg_file = sys.argv[1]
_cfg = parse_config_file(_cfg_file)
if _cfg['pred_model_download'] == "none":
print("Model download not enabled.")
sys.exit(1)
predictor_download_model(_cfg['pred_model_download'])
print(available_gfs(_cfg['pred_gfs_directory']))

Wyświetl plik

@ -21,6 +21,7 @@ from horuslib.atmosphere import time_to_landing
from horuslib.listener import OziListener, UDPListener
from horuslib.earthmaths import *
from chasemapper.config import *
from chasemapper.predictor import predictor_spawn_download, model_download_running
# Define Flask Application, and allow automatic reloading of templates for dev work
@ -198,7 +199,9 @@ def predictorThread():
for i in range(int(chasemapper_config['pred_update_rate'])):
time.sleep(1)
if predictor_thread_running == False:
return
break
logging.info("Closed predictor loop.")
def run_prediction():
@ -212,6 +215,13 @@ def run_prediction():
predictor_semaphore = True
for _payload in current_payload_tracks:
# Check the age of the data.
# No point re-running the predictor if the data is older than 30 seconds.
_pos_age = current_payloads[_payload]['telem']['server_time']
if (time.time()-_pos_age) > 30.0:
logging.debug("Skipping prediction for %s due to old data." % _payload)
continue
_current_pos = current_payload_tracks[_payload].get_latest_state()
_current_pos_list = [0,_current_pos['lat'], _current_pos['lon'], _current_pos['alt']]
@ -351,9 +361,10 @@ def initPredictor():
flask_emit_event('predictor_model_update',{'model':_model_age})
predictor = Predictor(bin_path=pred_settings['pred_binary'], gfs_path=pred_settings['gfs_path'])
# Start up the predictor thread.
predictor_thread = Thread(target=predictorThread)
predictor_thread.start()
# Start up the predictor thread if it is not running.
if predictor_thread == None:
predictor_thread = Thread(target=predictorThread)
predictor_thread.start()
# Set the predictor to enabled, and update the clients.
chasemapper_config['pred_enabled'] = True
@ -369,12 +380,58 @@ def initPredictor():
predictor = None
def model_download_finished(result):
""" Callback for when the model download is finished """
if result == "OK":
# Downloader reported OK, restart the predictor.
initPredictor()
else:
# Downloader reported an error, pass on to the client.
flask_emit_event('predictor_model_update',{'model':result})
@socketio.on('download_model', namespace='/chasemapper')
def download_new_model(data):
""" Trigger a download of a new weather model """
global pred_settings, model_download_running
# Don't action anything if there is a model download already running
logging.info("Web Client Initiated request for new predictor data.")
pass
# TODO
if pred_settings['pred_model_download'] == "none":
logging.info("No GFS model download command specified.")
flask_emit_event('predictor_model_update',{'model':"No model download cmd."})
return
else:
_model_cmd = pred_settings['pred_model_download']
flask_emit_event('predictor_model_update',{'model':"Downloading Model."})
_status = predictor_spawn_download(_model_cmd, model_download_finished)
flask_emit_event('predictor_model_update',{'model':_status})
# Data Clearing Functions
@socketio.on('payload_data_clear', namespace='/chasemapper')
def clear_payload_data(data):
""" Clear the payload data store """
global predictor_semaphore, current_payloads
logging.warning("Client requested all payload data be cleared.")
# Wait until any current predictions have finished running.
while predictor_semaphore:
time.sleep(0.1)
current_payloads = {}
current_payload_tracks = {}
@socketio.on('car_data_clear', namespace='/chasemapper')
def clear_car_data(data):
""" Clear out the car position track """
global car_track
logging.warning("Client requested all chase car data be cleared.")
car_track = GenericTrack()
# Incoming telemetry handlers
@ -479,7 +536,7 @@ if __name__ == "__main__":
pred_settings = {
'pred_binary': chasemapper_config['pred_binary'],
'gfs_path': chasemapper_config['pred_gfs_directory'],
'model_download': chasemapper_config['pred_model_download']
'pred_model_download': chasemapper_config['pred_model_download']
}
running_threads = []
@ -499,6 +556,7 @@ if __name__ == "__main__":
_summary_callback = None
if chasemapper_config['car_gps_source'] == "horus_udp":
logging.info("Listening for Chase Car position via Horus UDP.")
_gps_callback = udp_listener_car_callback
else:
_gps_callback = None

Wyświetl plik

@ -77,6 +77,7 @@
// if we haven't received data in a while.
var payload_data_age = 0.0;
var car_data_age = 0.0;
var pred_data_age = 0.0;
// Other markers which may be added. (TBD, probably other chase car positions via the LoRa payload?)
var misc_markers = {};
@ -174,6 +175,13 @@
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Experimental offline maps using a local tilestache server.
var offline_osm_map = L.tileLayer(location.protocol + '//' + document.domain + ':8080/roads/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
minNativeZoom:9,
maxNativeZoom:13
});
// Add ESRI Satellite Maps.
var esrimapLink =
'<a href="http://www.esri.com/">Esri</a>';
@ -185,7 +193,7 @@
attribution: '&copy; '+esrimapLink+', '+esriwholink,
maxZoom: 18,
});
map.addControl(new L.Control.Layers({'OSM':osm_map, 'ESRI Satellite':esri_sat_map}));
map.addControl(new L.Control.Layers({'OSM':osm_map, 'ESRI Satellite':esri_sat_map, 'Offline OSM': offline_osm_map}));
var sidebar = L.control.sidebar('sidebar').addTo(map);
// Add custom controls, which show various sets of data.
@ -223,7 +231,7 @@
// Data age display - shows how old the various datasets are.
L.control.custom({
position: 'topleft',
content : "<div class='dataAgeHeader'>Data Age</div><div id='payload_age' class='dataAgeOK'></div><div id='car_age' class='dataAgeOK'></div>",
content : "<div class='dataAgeHeader'>Data Age</div><div id='payload_age' class='dataAgeOK'></div><div id='car_age' class='dataAgeOK'></div><div id='pred_age' class='dataAgeOK'></div>",
classes : 'btn-group-vertical btn-group-sm',
id: 'age_display',
style :
@ -455,7 +463,7 @@
// Create marker!
chase_car_position.marker = L.marker(chase_car_position.latest_data,{title:"Chase Car", icon: carIcon})
.addTo(map);
chase_car_position.path = L.polyline([chase_car_position.latest_data],{title:"Chase Car", color:'black'});
chase_car_position.path = L.polyline([chase_car_position.latest_data],{title:"Chase Car", color:'black', weight:1.5});
// If the user wants the chase car tail, add it to the map.
if (document.getElementById("chaseCarTrack").checked == true){
chase_car_position.path.addTo(map);
@ -549,6 +557,12 @@
var _pred_path = data.pred_path;
var _pred_landing = data.pred_landing;
// It's possible (though unlikely) that we get sent a prediction track before telemetry data.
// In this case, just return.
if (balloon_positions.hasOwnProperty(data.callsign) == false){
return;
}
// Add the landing marker if it doesnt exist.
if (balloon_positions[_callsign].pred_marker == null){
balloon_positions[_callsign].pred_marker = L.marker(data.pred_landing,{title:_callsign + " Landing", icon: balloonLandingIcons[balloon_positions[_callsign].colour]})
@ -598,12 +612,61 @@
balloon_positions[_callsign].abort_marker.remove();
}
}
// Reset the prediction data age counter.
pred_data_age = 0.0;
});
$("#downloadModel").click(function(){
socket.emit('download_model', {data: 'plzkthx'});
});
$("#clearPayloadData").click(function(){
var _confirm = confirm("Really clear all Payload data?");
if (_confirm == true){
socket.emit('payload_data_clear', {data: 'plzkthx'});
// Clear all payload markers and tracks from the map/
for (_callsign in balloon_positions){
balloon_positions[_callsign].marker.remove();
balloon_positions[_callsign].path.remove();
balloon_positions[_callsign].pred_path.remove();
balloon_positions[_callsign].abort_path.remove();
// Clear out the markers if they exist.
if (balloon_positions[_callsign].abort_marker != null){
balloon_positions[_callsign].abort_marker.remove();
}
if (balloon_positions[_callsign].burst_marker != null){
balloon_positions[_callsign].burst_marker.remove();
}
if (balloon_positions[_callsign].pred_marker != null){
balloon_positions[_callsign].pred_marker.remove();
}
}
// Reset the balloon positions object to nothing.
balloon_positions = {};
balloon_currently_following = "none";
// Update tables.
updateTelemetryTable();
updateSummaryDisplay();
}
});
$("#clearCarData").click(function(){
var _confirm = confirm("Really clear all Chase Car data?");
if (_confirm == true){
socket.emit('car_data_clear', {data: 'plzkthx'});
if (chase_car_position.marker !== "NONE"){
chase_car_position.marker.remove();
}
if (chase_car_position.path !== "NONE"){
chase_car_position.path.remove();
}
chase_car_position = {latest_data: [], heading:0, marker: 'NONE', path: 'NONE'};
}
});
// Tell the server we are connected and ready for data.
socket.on('connect', function() {
socket.emit('client_connected', {data: 'I\'m connected!'});
@ -616,6 +679,7 @@
// Update the data age displays.
payload_data_age += age_update_rate/1000.0;
car_data_age += age_update_rate/1000.0;
pred_data_age += age_update_rate/1000.0;
if (balloon_currently_following === 'none'){
$("#payload_age").text("Payload: No Data.");
@ -642,6 +706,12 @@
$("#car_age").addClass('dataAgeOK');
}
}
if (chase_config['pred_enabled']==false){
$("#pred_age").text("Predictions: No Data.");
}else{
$("#pred_age").text("Predictions: " + pred_data_age.toFixed(1)+"s");
}
}, age_update_rate);
});
</script>
@ -707,6 +777,15 @@
<div class="paramRow">
<b>Update Rate</b><input type="text" class="paramEntry" id="predUpdateRate"><br/>
</div>
</hr>
<h4>Other</h4>
<div class="paramRow">
<button type="button" class="paramSelector" id="clearPayloadData">Clear Payload Data</button></br>
</div>
<div class="paramRow">
<button type="button" class="paramSelector" id="clearCarData">Clear Chase-Car Track</button></br>
</div>
</div>
</div>
</div>