Added abort prediction support, dynamic updates of predictor settings.

bearings
Mark Jessop 2018-07-17 22:53:59 +09:30
rodzic b2d5f46859
commit ca6180f7bf
2 zmienionych plików z 146 dodań i 54 usunięć

Wyświetl plik

@ -34,7 +34,13 @@ socketio = SocketIO(app)
# Global stores of data. # Global stores of data.
# Don't expose these settings to the client!
pred_settings = {
'pred_binary': "./pred",
'gfs_path': "./gfs/",
}
# These settings are shared between server and all clients, and are updated dynamically.
chasemapper_config = { chasemapper_config = {
# Start location for the map (until either a chase car position, or balloon position is available.) # Start location for the map (until either a chase car position, or balloon position is available.)
'default_lat': -34.9, 'default_lat': -34.9,
@ -43,13 +49,11 @@ chasemapper_config = {
# Predictor settings # Predictor settings
'pred_enabled': False, # Enable running and display of predicted flight paths. 'pred_enabled': False, # Enable running and display of predicted flight paths.
# 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': "No Data", 'pred_model': "Disabled",
'pred_desc_rate': 6.0, 'pred_desc_rate': 6.0,
'pred_burst': 28000, 'pred_burst': 28000,
'show_abort': True, # Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*) 'show_abort': True, # Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*)
'pred_binary': "./pred", 'pred_update_rate': 15 # Update predictor every 15 seconds.
'gfs_path': "./gfs/",
'predictor_update_rate': 15 # Update predictor every 15 seconds.
} }
# Payload data Stores # Payload data Stores
@ -80,12 +84,26 @@ def flask_get_config():
return json.dumps(chasemapper_config) return json.dumps(chasemapper_config)
def flask_emit_event(event_name="none", data={}): def flask_emit_event(event_name="none", data={}):
""" Emit a socketio event to any clients. """ """ Emit a socketio event to any clients. """
socketio.emit(event_name, data, namespace='/chasemapper') socketio.emit(event_name, data, namespace='/chasemapper')
@socketio.on('client_settings_update', namespace='/chasemapper')
def client_settings_update(data):
global chasemapper_config
# Overwrite local config data with data from the client.
# TODO: Some sanitization of this data... this could lead to bad things.
chasemapper_config = data
# Updates based on
# Push settings back out to all clients.
flask_emit_event('server_settings_update', chasemapper_config)
def handle_new_payload_position(data): def handle_new_payload_position(data):
_lat = data['lat'] _lat = data['lat']
@ -162,6 +180,7 @@ def handle_new_payload_position(data):
# Predictor Code # Predictor Code
# #
predictor = None predictor = None
predictor_semaphore = False
predictor_thread_running = True predictor_thread_running = True
predictor_thread = None predictor_thread = None
@ -172,7 +191,7 @@ def predictorThread():
while predictor_thread_running: while predictor_thread_running:
run_prediction() run_prediction()
for i in range(int(chasemapper_config['predictor_update_rate'])): for i in range(int(chasemapper_config['pred_update_rate'])):
time.sleep(1) time.sleep(1)
if predictor_thread_running == False: if predictor_thread_running == False:
return return
@ -182,9 +201,11 @@ def run_prediction():
''' Run a Flight Path prediction ''' ''' Run a Flight Path prediction '''
global chasemapper_config, current_payloads, current_payload_tracks, predictor global chasemapper_config, current_payloads, current_payload_tracks, predictor
if predictor == None: if (predictor == None) or (chasemapper_config['pred_enabled'] == False):
return return
# Set the semaphore so we don't accidentally kill the predictor object while it's running.
predictor_semaphore = True
for _payload in current_payload_tracks: for _payload in current_payload_tracks:
_current_pos = current_payload_tracks[_payload].get_latest_state() _current_pos = current_payload_tracks[_payload].get_latest_state()
@ -240,39 +261,52 @@ def run_prediction():
else: else:
logging.error("Prediction Failed.") logging.error("Prediction Failed.")
# if _run_abort_prediction and (_current_pos['alt'] < burst_alt) and (_current_pos['is_descending'] == False): # Abort predictions
# print("Running Abort Prediction... ") if chasemapper_config['show_abort'] and (_current_pos['alt'] < chasemapper_config['pred_burst']) and (_current_pos['is_descending'] == False):
# _pred_path = _predictor.predict( logging.info("Running Abort Predictor for: %s." % _payload)
# 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'])
# if len(_pred_path) > 1: _abort_pred_path = predictor.predict(
# _pred_path.insert(0,_current_pos_list) launch_lat=_current_pos['lat'],
# _abort_prediction = _pred_path launch_lon=_current_pos['lon'],
# _abort_prediction_valid = True launch_alt=_current_pos['alt'],
# print("Abort Prediction Updated, %d points." % len(_pred_path)) ascent_rate=_current_pos['ascent_rate'],
# else: descent_rate=_desc_rate,
# print("Prediction Failed.") burst_alt=_current_pos['alt']+200,
# else: launch_time=_current_pos['time'],
# _abort_prediction_valid = False descent_mode=_current_pos['is_descending'])
# # If have been asked to run an abort prediction, but we are descent, set the is_valid if len(_pred_path) > 1:
# # flag to false, so the abort prediction is not plotted. # Valid Prediction!
# if _run_abort_prediction and _current_pos['is_descending']: _abort_pred_path.insert(0,_current_pos_list)
# _abort_prediction_valid == False # Convert from predictor output format to a polyline.
_abort_pred_output = []
for _point in _abort_pred_path:
_abort_pred_output.append([_point[1], _point[2], _point[3]])
current_payloads[_payload]['abort_path'] = _abort_pred_output
current_payloads[_payload]['abort_landing'] = _abort_pred_output[-1]
logging.info("Abort Prediction Updated, %d data points." % len(_pred_path))
else:
logging.error("Prediction Failed.")
current_payloads[_payload]['abort_path'] = []
current_payloads[_payload]['abort_landing'] = []
else:
# Zero the abort path and landing
current_payloads[_payload]['abort_path'] = []
current_payloads[_payload]['abort_landing'] = []
predictor_semaphore = False
# Send the web client the updated prediction data.
_client_data = { _client_data = {
'callsign': _payload, 'callsign': _payload,
'pred_path': current_payloads[_payload]['pred_path'], 'pred_path': current_payloads[_payload]['pred_path'],
'pred_landing': current_payloads[_payload]['pred_landing'], 'pred_landing': current_payloads[_payload]['pred_landing'],
'burst': current_payloads[_payload]['burst'], 'burst': current_payloads[_payload]['burst'],
'abort_path': [], 'abort_path': current_payloads[_payload]['abort_path'],
'abort_landing': [] 'abort_landing': current_payloads[_payload]['abort_landing']
} }
flask_emit_event('predictor_update', _client_data) flask_emit_event('predictor_update', _client_data)
@ -284,7 +318,7 @@ def initPredictor():
from cusfpredict.utils import gfs_model_age from cusfpredict.utils import gfs_model_age
# Check if we have any GFS data # Check if we have any GFS data
_model_age = gfs_model_age(chasemapper_config['gfs_path']) _model_age = gfs_model_age(pred_settings['gfs_path'])
if _model_age == "Unknown": if _model_age == "Unknown":
logging.error("No GFS data in directory.") logging.error("No GFS data in directory.")
chasemapper_config['pred_model'] = "No GFS Data." chasemapper_config['pred_model'] = "No GFS Data."
@ -292,12 +326,16 @@ def initPredictor():
else: else:
chasemapper_config['pred_model'] = _model_age chasemapper_config['pred_model'] = _model_age
flask_emit_event('predictor_model_update',{'model':_model_age}) flask_emit_event('predictor_model_update',{'model':_model_age})
predictor = Predictor(bin_path=chasemapper_config['pred_binary'], gfs_path=chasemapper_config['gfs_path']) predictor = Predictor(bin_path=pred_settings['pred_binary'], gfs_path=pred_settings['gfs_path'])
# Start up the predictor thread. # Start up the predictor thread.
predictor_thread = Thread(target=predictorThread) predictor_thread = Thread(target=predictorThread)
predictor_thread.start() predictor_thread.start()
# Set the predictor to enabled, and update the clients.
chasemapper_config['pred_enabled'] = True
flask_emit_event('server_settings_update', chasemapper_config)
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
logging.error("Loading predictor failed: " + str(e)) logging.error("Loading predictor failed: " + str(e))
@ -310,6 +348,7 @@ def initPredictor():
@socketio.on('download_model', namespace='/chasemapper') @socketio.on('download_model', namespace='/chasemapper')
def download_new_model(data): def download_new_model(data):
""" Trigger a download of a new weather model """ """ Trigger a download of a new weather model """
logging.info("Web Client Initiated request for new predictor data.")
pass pass
# TODO # TODO

Wyświetl plik

@ -46,6 +46,7 @@
// 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_desc_rate: 6.0, pred_desc_rate: 6.0,
pred_burst: 28000, pred_burst: 28000,
pred_update_rate: 15,
pred_model: 'Disabled', pred_model: 'Disabled',
show_abort: true, // Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*) show_abort: true, // Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*)
}; };
@ -80,6 +81,7 @@
// Other markers which may be added. (TBD, probably other chase car positions via the LoRa payload?) // Other markers which may be added. (TBD, probably other chase car positions via the LoRa payload?)
var misc_markers = {}; var misc_markers = {};
var updateSettings;
$(document).ready(function() { $(document).ready(function() {
// Use the 'chasemapper' namespace for all of our traffic // Use the 'chasemapper' namespace for all of our traffic
@ -90,6 +92,19 @@
// http[s]://<domain>:<port>[/<namespace>] // http[s]://<domain>:<port>[/<namespace>]
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace); var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
function serverSettingsUpdate(data){
// Accept a json blob of settings data from the client, and update our local store.
chase_config = data;
// Update a few fields based on this data.
$("#predictorModel").html("<b>Current Model: </b>" + chase_config.pred_model);
$('#burstAlt').val(chase_config.pred_burst.toFixed(0));
$('#descentRate').val(chase_config.pred_desc_rate.toFixed(1));
$('#predUpdateRate').val(chase_config.pred_update_rate.toFixed(0));
$("#predictorEnabled").prop('checked', chase_config.pred_enabled);
$("#abortPredictionEnabled").prop('checked', chase_config.show_abort);
}
// Grab the System config on startup. // Grab the System config on startup.
// Refer to config.py for the contents of the configuration blob. // Refer to config.py for the contents of the configuration blob.
$.ajax({ $.ajax({
@ -97,13 +112,49 @@
dataType: 'json', dataType: 'json',
async: false, // Yes, this is deprecated... async: false, // Yes, this is deprecated...
success: function(data) { success: function(data) {
chase_config = data; serverSettingsUpdate(data);
// Update a few fields based on this data.
$("#predictorModel").html("<b>Current Model: </b>" + chase_config.pred_model);
$('#burstAlt').val(chase_config.pred_burst.toFixed(0));
$('#descentRate').val(chase_config.pred_desc_rate.toFixed(1));
} }
}); });
// Handler for further settings updates.
socket.on('server_settings_update', function(data){
serverSettingsUpdate(data);
});
// Settings updates.
updateSettings = function(){
chase_config.pred_enabled = document.getElementById("predictorEnabled").checked;
chase_config.show_abort = document.getElementById("abortPredictionEnabled").checked;
// Attempt to parse the text field values.
var _burst_alt = parseFloat($('#burstAlt').val());
if (isNaN(_burst_alt) == false){
chase_config.pred_burst = _burst_alt;
}
var _desc_rate = parseFloat($('#descentRate').val());
if (isNaN(_desc_rate) == false){
chase_config.pred_desc_rate = _desc_rate
}
var _update_rate = parseInt($('#predUpdateRate').val());
if (isNaN(_update_rate) == false){
chase_config.pred_update_rate = _update_rate
}
socket.emit('client_settings_update', chase_config);
};
// Use the jquery on-changed call for text entry fields,
// so they only fire after they lose focus.
$("#burstAlt").change(function(){
updateSettings();
});
$("#descentRate").change(function(){
updateSettings();
});
$("#predUpdateRate").change(function(){
updateSettings();
});
// Event handler for Log data. // Event handler for Log data.
socket.on('log_event', function(msg) { socket.on('log_event', function(msg) {
@ -525,21 +576,19 @@
// Update the predicted path. // Update the predicted path.
balloon_positions[_callsign].pred_path.setLatLngs(data.pred_path); balloon_positions[_callsign].pred_path.setLatLngs(data.pred_path);
if (data.hasOwnProperty("abort_landing") == true){ if (data.abort_landing.length == 3){
// Only update the abort data if there is actually abort data to show. // Only update the abort data if there is actually abort data to show.
if(data.abort_landing.length == 3){ if (balloon_positions[_callsign].abort_marker == null){
if (balloon_positions[_callsign].abort_marker == null){ balloon_positions[callsign].abort_marker = L.marker(data.abort_landing,{title:callsign + " Abort", icon: abortIcon})
balloon_positions[callsign].abort_marker = L.marker(data.abort_landing,{title:callsign + " Abort", icon: abortIcon}) .bindTooltip(callsign + " Abort Landing",{permanent:false,direction:'right'});
.bindTooltip(callsign + " Abort Landing",{permanent:false,direction:'right'}); if(chase_config.show_abort == true){
if(chase_config.show_abort == true){ balloon_positions[callsign].abort_marker.addTo(map);
balloon_positions[callsign].abort_marker.addTo(map);
}
}else{
balloon_positions[callsign].abort_marker.setLatLng(data.abort_landing);
} }
}else{
balloon_positions[_callsign].abort_path.setLatLngs(data.abort_path); balloon_positions[callsign].abort_marker.setLatLng(data.abort_landing);
} }
balloon_positions[_callsign].abort_path.setLatLngs(data.abort_path);
}else{ }else{
// Clear out the abort and abort marker data. // Clear out the abort and abort marker data.
balloon_positions[_callsign].abort_path.setLatLngs([]); balloon_positions[_callsign].abort_path.setLatLngs([]);
@ -593,8 +642,6 @@
} }
} }
}, age_update_rate); }, age_update_rate);
}); });
</script> </script>
</head> </head>
@ -645,7 +692,10 @@
<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>
</div> </div>
<div class="paramRow"> <div class="paramRow">
<b>Enable Predictions</b> <input type="checkbox" class="paramSelector" id="predictorEnabled"> <b>Enable Predictions</b> <input type="checkbox" class="paramSelector" id="predictorEnabled" onclick='updateSettings();'>
</div>
<div class="paramRow">
<b>Show 'Abort' Predictions</b> <input type="checkbox" class="paramSelector" id="abortPredictionEnabled" onclick='updateSettings();'>
</div> </div>
<div class="paramRow"> <div class="paramRow">
<b>Burst Altitude</b><input type="text" class="paramEntry" id="burstAlt"><br/> <b>Burst Altitude</b><input type="text" class="paramEntry" id="burstAlt"><br/>
@ -653,6 +703,9 @@
<div class="paramRow"> <div class="paramRow">
<b>Descent Rate</b><input type="text" class="paramEntry" id="descentRate"><br/> <b>Descent Rate</b><input type="text" class="paramEntry" id="descentRate"><br/>
</div> </div>
<div class="paramRow">
<b>Update Rate</b><input type="text" class="paramEntry" id="predUpdateRate"><br/>
</div>
</div> </div>
</div> </div>
</div> </div>