diff --git a/chasemapper.py b/chasemapper.py index 17d5624..5906d8f 100644 --- a/chasemapper.py +++ b/chasemapper.py @@ -34,7 +34,13 @@ socketio = SocketIO(app) # 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 = { # Start location for the map (until either a chase car position, or balloon position is available.) 'default_lat': -34.9, @@ -43,13 +49,11 @@ chasemapper_config = { # Predictor settings 'pred_enabled': False, # Enable running and display of predicted flight paths. # 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_burst': 28000, 'show_abort': True, # Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*) - 'pred_binary': "./pred", - 'gfs_path': "./gfs/", - 'predictor_update_rate': 15 # Update predictor every 15 seconds. + 'pred_update_rate': 15 # Update predictor every 15 seconds. } # Payload data Stores @@ -80,12 +84,26 @@ def flask_get_config(): return json.dumps(chasemapper_config) - def flask_emit_event(event_name="none", data={}): """ Emit a socketio event to any clients. """ 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): _lat = data['lat'] @@ -162,6 +180,7 @@ def handle_new_payload_position(data): # Predictor Code # predictor = None +predictor_semaphore = False predictor_thread_running = True predictor_thread = None @@ -172,7 +191,7 @@ def predictorThread(): while predictor_thread_running: 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) if predictor_thread_running == False: return @@ -182,9 +201,11 @@ def run_prediction(): ''' Run a Flight Path prediction ''' global chasemapper_config, current_payloads, current_payload_tracks, predictor - if predictor == None: + if (predictor == None) or (chasemapper_config['pred_enabled'] == False): 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: _current_pos = current_payload_tracks[_payload].get_latest_state() @@ -240,39 +261,52 @@ def run_prediction(): else: logging.error("Prediction Failed.") - # if _run_abort_prediction and (_current_pos['alt'] < burst_alt) and (_current_pos['is_descending'] == False): - # print("Running Abort Prediction... ") - # _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']) + # Abort predictions + 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) - # if len(_pred_path) > 1: - # _pred_path.insert(0,_current_pos_list) - # _abort_prediction = _pred_path - # _abort_prediction_valid = True - # print("Abort Prediction Updated, %d points." % len(_pred_path)) - # else: - # print("Prediction Failed.") - # else: - # _abort_prediction_valid = False + _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 have been asked to run an abort prediction, but we are descent, set the is_valid - # # flag to false, so the abort prediction is not plotted. - # if _run_abort_prediction and _current_pos['is_descending']: - # _abort_prediction_valid == False + if len(_pred_path) > 1: + # Valid Prediction! + _abort_pred_path.insert(0,_current_pos_list) + # 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 = { 'callsign': _payload, 'pred_path': current_payloads[_payload]['pred_path'], 'pred_landing': current_payloads[_payload]['pred_landing'], 'burst': current_payloads[_payload]['burst'], - 'abort_path': [], - 'abort_landing': [] + 'abort_path': current_payloads[_payload]['abort_path'], + 'abort_landing': current_payloads[_payload]['abort_landing'] } flask_emit_event('predictor_update', _client_data) @@ -284,7 +318,7 @@ def initPredictor(): from cusfpredict.utils import gfs_model_age # 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": logging.error("No GFS data in directory.") chasemapper_config['pred_model'] = "No GFS Data." @@ -292,12 +326,16 @@ def initPredictor(): else: chasemapper_config['pred_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. predictor_thread = Thread(target=predictorThread) 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: traceback.print_exc() logging.error("Loading predictor failed: " + str(e)) @@ -310,6 +348,7 @@ def initPredictor(): @socketio.on('download_model', namespace='/chasemapper') def download_new_model(data): """ Trigger a download of a new weather model """ + logging.info("Web Client Initiated request for new predictor data.") pass # TODO diff --git a/templates/index.html b/templates/index.html index b99d004..33442d8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -46,6 +46,7 @@ // Default prediction settings (actual values will be used once the flight is underway) pred_desc_rate: 6.0, pred_burst: 28000, + pred_update_rate: 15, pred_model: 'Disabled', 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?) var misc_markers = {}; + var updateSettings; $(document).ready(function() { // Use the 'chasemapper' namespace for all of our traffic @@ -90,6 +92,19 @@ // http[s]://:[/] 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("Current Model: " + 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. // Refer to config.py for the contents of the configuration blob. $.ajax({ @@ -97,13 +112,49 @@ dataType: 'json', async: false, // Yes, this is deprecated... success: function(data) { - chase_config = data; - // Update a few fields based on this data. - $("#predictorModel").html("Current Model: " + chase_config.pred_model); - $('#burstAlt').val(chase_config.pred_burst.toFixed(0)); - $('#descentRate').val(chase_config.pred_desc_rate.toFixed(1)); + serverSettingsUpdate(data); } }); + // 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. socket.on('log_event', function(msg) { @@ -525,21 +576,19 @@ // Update the predicted 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. - if(data.abort_landing.length == 3){ - if (balloon_positions[_callsign].abort_marker == null){ - balloon_positions[callsign].abort_marker = L.marker(data.abort_landing,{title:callsign + " Abort", icon: abortIcon}) - .bindTooltip(callsign + " Abort Landing",{permanent:false,direction:'right'}); - if(chase_config.show_abort == true){ - balloon_positions[callsign].abort_marker.addTo(map); - } - }else{ - balloon_positions[callsign].abort_marker.setLatLng(data.abort_landing); + if (balloon_positions[_callsign].abort_marker == null){ + balloon_positions[callsign].abort_marker = L.marker(data.abort_landing,{title:callsign + " Abort", icon: abortIcon}) + .bindTooltip(callsign + " Abort Landing",{permanent:false,direction:'right'}); + if(chase_config.show_abort == true){ + balloon_positions[callsign].abort_marker.addTo(map); } - - balloon_positions[_callsign].abort_path.setLatLngs(data.abort_path); + }else{ + balloon_positions[callsign].abort_marker.setLatLng(data.abort_landing); } + + balloon_positions[_callsign].abort_path.setLatLngs(data.abort_path); }else{ // Clear out the abort and abort marker data. balloon_positions[_callsign].abort_path.setLatLngs([]); @@ -593,8 +642,6 @@ } } }, age_update_rate); - - }); @@ -645,7 +692,10 @@ Download Model
- Enable Predictions + Enable Predictions +
+
+ Show 'Abort' Predictions
Burst Altitude
@@ -653,6 +703,9 @@
Descent Rate
+
+ Update Rate
+