From 9627bbb43604a3a1c6ffe5225ee5a9715f93e182 Mon Sep 17 00:00:00 2001 From: Mark Jessop Date: Sun, 22 Jul 2018 22:03:55 +0930 Subject: [PATCH] Add data flush option. Add experimental offline map layer. --- chasemapper/predictor.py | 71 +++++++++++++++++++++++++++++++++ horusmapper.py | 72 ++++++++++++++++++++++++++++++---- templates/index.html | 85 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 218 insertions(+), 10 deletions(-) diff --git a/chasemapper/predictor.py b/chasemapper/predictor.py index bfd1a2b..903f2df 100644 --- a/chasemapper/predictor.py +++ b/chasemapper/predictor.py @@ -5,3 +5,74 @@ # Copyright (C) 2018 Mark Jessop # 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'])) + diff --git a/horusmapper.py b/horusmapper.py index 52ce093..0a29538 100644 --- a/horusmapper.py +++ b/horusmapper.py @@ -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 diff --git a/templates/index.html b/templates/index.html index 0302bc9..8c96be7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -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: '© OpenStreetMap 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: '© OpenStreetMap contributors', + minNativeZoom:9, + maxNativeZoom:13 + }); + // Add ESRI Satellite Maps. var esrimapLink = 'Esri'; @@ -185,7 +193,7 @@ attribution: '© '+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 : "
Data Age
", + content : "
Data Age
", 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); }); @@ -707,6 +777,15 @@
Update Rate
+ +

Other

+
+
+
+
+
+
+