Add chasemapper module. Settings now loaded from configuration file.

bearings
Mark Jessop 2018-07-19 22:14:01 +09:30
rodzic 2888e5ad07
commit cfb4e13aed
6 zmienionych plików z 300 dodań i 88 usunięć

Wyświetl plik

@ -0,0 +1,7 @@
#!/usr/bin/env python
#
# Project Horus - Browser-Based Chase Mapper
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#

Wyświetl plik

@ -0,0 +1,90 @@
#!/usr/bin/env python
#
# Project Horus - Browser-Based Chase Mapper - Config Reader
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
import logging
try:
# Python 2
from ConfigParser import RawConfigParser
except ImportError:
# Python 3
from configparser import RawConfigParser
default_config = {
# Start location for the map (until either a chase car position, or balloon position is available.)
'default_lat': -34.9,
'default_lon': 138.6,
# 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': "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_update_rate': 15 # Update predictor every 15 seconds.
}
def parse_config_file(filename):
""" Parse a Configuration File """
chase_config = default_config.copy()
config = RawConfigParser()
config.read(filename)
# Map Defaults
chase_config['flask_host'] = config.get('map', 'flask_host')
chase_config['flask_port'] = config.getint('map', 'flask_port')
chase_config['default_lat'] = config.get('map', 'default_lat')
chase_config['default_lon'] = config.get('map', 'default_lon')
# Source Selection
chase_config['data_source'] = config.get('source', 'type')
chase_config['ozimux_port'] = config.getint('source', 'ozimux_port')
chase_config['horus_udp_port'] = config.getint('source', 'horus_udp_port')
# Car GPS Data
chase_config['car_gps_source'] = config.get('car_gps','source')
chase_config['car_gpsd_host'] = config.get('car_gps','gpsd_host')
chase_config['car_gpsd_port'] = config.getint('car_gps','gpsd_port')
# Predictor
chase_config['pred_enabled'] = config.getboolean('predictor', 'predictor_enabled')
chase_config['pred_burst'] = config.getfloat('predictor', 'default_burst')
chase_config['pred_desc_rate'] = config.getfloat('predictor', 'default_descent_rate')
chase_config['pred_binary'] = config.get('predictor','pred_binary')
chase_config['pred_gfs_directory'] = config.get('predictor', 'gfs_directory')
chase_config['pred_model_download'] = config.get('predictor', 'model_download')
return chase_config
def read_config(filename, default_cfg="horusmapper.cfg.example"):
""" Read in a Horus Mapper configuration file,and return as a dict. """
try:
config_dict = parse_config_file(filename)
except Exception as e:
logging.error("Could not parse %s, trying default: %s" % (filename, str(e)))
try:
config_dict = parse_config_file(default_cfg)
except Exception as e:
logging.critical("Could not parse example config file! - %s" % str(e))
config_dict = None
return config_dict
if __name__ == "__main__":
import sys
print(read_config(sys.argv[1]))

Wyświetl plik

@ -0,0 +1,7 @@
#!/usr/bin/env python
#
# Project Horus - Browser-Based Chase Mapper - Predictor
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#

Wyświetl plik

@ -0,0 +1,68 @@
#
# Project Horus Chase-Mapper Configuration File
#
# Copy this file to horusmapper.cfg and modify as required.
#
# Telemetry Data Source
[source]
# Data source type:
# ozimux - Read data in OziMux format
# horus_udp - Read Horus UDP Broadcast 'Payload Summary' messages
type = ozimux
# Ozimux Data source UDP port
ozimux_port = 8942
# 'Payload Summary' (also car GPS messages) UDP listen port
horus_udp_port = 55672
# Chase-Car Position Settings
[car_gps]
# Chase car Position data Source
# none - No Chase-Car GPS
# horus_udp - Read Horus UDP Broadcast 'Car GPS' messages
# gpsd - Poll GPSD for positions. (TO BE IMPLEMENTED)
source = horus_udp
# GPSD Host/Port
# TO BE IMPLEMENTED
gpsd_host = localhost
gpsd_port = 2947
# Map Defaults
[map]
# Host/port to host webserver on
flask_host = 0.0.0.0
flask_port = 5001
# Default map centre
default_lat = -34.9
default_lon = 138.6
# Predictor Settings
# Use of the 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.
[predictor]
# Enable Predictor (True/False)
predictor_enabled = False
# Predictor defaults - these can be modified at runtime in the web interface.
default_burst = 30000
default_descent_rate = 5.0
# Predictory Binary Location
# Where to find the built CUSF predictor binary. This will usually be ./pred or pred.exe (on Windows)
pred_binary = ./pred
# Directory containing GFS model data.
gfs_directory = ./gfs/
# Wind Model Download Command
# Optional command to enable downloading of wind data via a web client button.
# Example: (this will require copying the get_wind_data.py script to this dirctory)
# model_download = python get_wind_data.py --lat=-33 --lon=139 --latdelta=10 --londelta=10 -f 24 -m 0p50 -o gfs
# The gfs directory (above) will be cleared of all .dat files prior to the above command being run.
model_download = none

Wyświetl plik

@ -13,13 +13,14 @@ import sys
import time import time
import traceback import traceback
from threading import Thread from threading import Thread
from datetime import datetime from datetime import datetime, timedelta
from dateutil.parser import parse from dateutil.parser import parse
from horuslib import * from horuslib import *
from horuslib.geometry import * from horuslib.geometry import *
from horuslib.atmosphere import time_to_landing from horuslib.atmosphere import time_to_landing
from horuslib.listener import OziListener, UDPListener from horuslib.listener import OziListener, UDPListener
from horuslib.earthmaths import * from horuslib.earthmaths import *
from chasemapper.config import *
# Define Flask Application, and allow automatic reloading of templates for dev work # Define Flask Application, and allow automatic reloading of templates for dev work
@ -34,27 +35,12 @@ 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. # 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.)
'default_lat': -34.9,
'default_lon': 138.6,
# Predictor settings # These settings are not editable by the client!
'pred_enabled': False, # Enable running and display of predicted flight paths. pred_settings = {}
# Default prediction settings (actual values will be used once the flight is underway)
'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_update_rate': 15 # Update predictor every 15 seconds.
}
# Payload data Stores # Payload data Stores
current_payloads = {} # Archive data which will be passed to the web client current_payloads = {} # Archive data which will be passed to the web client
@ -93,11 +79,28 @@ def flask_emit_event(event_name="none", data={}):
def client_settings_update(data): def client_settings_update(data):
global chasemapper_config global chasemapper_config
_predictor_change = "none"
if (chasemapper_config['pred_enabled'] == False) and (data['pred_enabled'] == True):
_predictor_change = "restart"
elif (chasemapper_config['pred_enabled'] == True) and (data['pred_enabled'] == False):
_predictor_change = "stop"
# Overwrite local config data with data from the client. # Overwrite local config data with data from the client.
# TODO: Some sanitization of this data... this could lead to bad things.
chasemapper_config = data chasemapper_config = data
# Updates based on if _predictor_change == "restart":
# Wait until any current predictions have finished.
while predictor_semaphore:
time.sleep(0.1)
# Attempt to start the predictor.
initPredictor()
elif _predictor_change == "stop":
# Wait until any current predictions have finished.
while predictor_semaphore:
time.sleep(0.1)
predictor = None
# Push settings back out to all clients. # Push settings back out to all clients.
flask_emit_event('server_settings_update', chasemapper_config) flask_emit_event('server_settings_update', chasemapper_config)
@ -119,7 +122,7 @@ def handle_new_payload_position(data):
current_payload_tracks[_callsign] = GenericTrack() current_payload_tracks[_callsign] = GenericTrack()
current_payloads[_callsign] = { current_payloads[_callsign] = {
'telem': {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':0.0, 'speed':0.0, 'short_time':_short_time, 'time_to_landing':""}, 'telem': {'callsign': _callsign, 'position':[_lat, _lon, _alt], 'vel_v':0.0, 'speed':0.0, 'short_time':_short_time, 'time_to_landing':"", 'server_time':time.time()},
'path': [], 'path': [],
'pred_path': [], 'pred_path': [],
'pred_landing': [], 'pred_landing': [],
@ -168,7 +171,8 @@ def handle_new_payload_position(data):
'vel_v':_vel_v, 'vel_v':_vel_v,
'speed':_speed, 'speed':_speed,
'short_time':_short_time, 'short_time':_short_time,
'time_to_landing': _ttl} 'time_to_landing': _ttl,
'server_time':time.time()}
current_payloads[_callsign]['path'].append([_lat, _lon, _alt]) current_payloads[_callsign]['path'].append([_lat, _lon, _alt])
@ -211,6 +215,9 @@ def run_prediction():
_current_pos = current_payload_tracks[_payload].get_latest_state() _current_pos = current_payload_tracks[_payload].get_latest_state()
_current_pos_list = [0,_current_pos['lat'], _current_pos['lon'], _current_pos['alt']] _current_pos_list = [0,_current_pos['lat'], _current_pos['lon'], _current_pos['alt']]
_pred_ok = False
_abort_pred_ok = False
if _current_pos['is_descending']: if _current_pos['is_descending']:
_desc_rate = _current_pos['landing_rate'] _desc_rate = _current_pos['landing_rate']
else: else:
@ -256,9 +263,12 @@ def run_prediction():
current_payloads[_payload]['burst'] = _pred_output[_cur_idx] current_payloads[_payload]['burst'] = _pred_output[_cur_idx]
_pred_ok = True
logging.info("Prediction Updated, %d data points." % len(_pred_path)) logging.info("Prediction Updated, %d data points." % len(_pred_path))
else: else:
current_payloads[_payload]['pred_path'] = []
current_payloads[_payload]['pred_landing'] = []
current_payloads[_payload]['burst'] = []
logging.error("Prediction Failed.") logging.error("Prediction Failed.")
# Abort predictions # Abort predictions
@ -286,7 +296,7 @@ def run_prediction():
current_payloads[_payload]['abort_path'] = _abort_pred_output current_payloads[_payload]['abort_path'] = _abort_pred_output
current_payloads[_payload]['abort_landing'] = _abort_pred_output[-1] current_payloads[_payload]['abort_landing'] = _abort_pred_output[-1]
_abort_pred_ok = True
logging.info("Abort Prediction Updated, %d data points." % len(_pred_path)) logging.info("Abort Prediction Updated, %d data points." % len(_pred_path))
else: else:
logging.error("Prediction Failed.") logging.error("Prediction Failed.")
@ -300,6 +310,7 @@ def run_prediction():
predictor_semaphore = False predictor_semaphore = False
# Send the web client the updated prediction data. # Send the web client the updated prediction data.
if _pred_ok or _abort_pred_ok:
_client_data = { _client_data = {
'callsign': _payload, 'callsign': _payload,
'pred_path': current_payloads[_payload]['pred_path'], 'pred_path': current_payloads[_payload]['pred_path'],
@ -312,10 +323,10 @@ def run_prediction():
def initPredictor(): def initPredictor():
global predictor, predictor_thread, chasemapper_config global predictor, predictor_thread, chasemapper_config, pred_settings
try: try:
from cusfpredict.predict import Predictor from cusfpredict.predict import Predictor
from cusfpredict.utils import gfs_model_age from cusfpredict.utils import gfs_model_age, available_gfs
# Check if we have any GFS data # Check if we have any GFS data
_model_age = gfs_model_age(pred_settings['gfs_path']) _model_age = gfs_model_age(pred_settings['gfs_path'])
@ -323,6 +334,18 @@ def initPredictor():
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."
flask_emit_event('predictor_model_update',{'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
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})
@ -334,6 +357,7 @@ def initPredictor():
# Set the predictor to enabled, and update the clients. # Set the predictor to enabled, and update the clients.
chasemapper_config['pred_enabled'] = True chasemapper_config['pred_enabled'] = True
flask_emit_event('server_settings_update', chasemapper_config) flask_emit_event('server_settings_update', chasemapper_config)
except Exception as e: except Exception as e:
@ -426,18 +450,7 @@ def udp_listener_car_callback(data):
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group() parser.add_argument("-c", "--config", type=str, default="horusmapper.cfg", help="Configuration file.")
parser.add_argument("-p","--port",default=5001,help="Port to run Web Server on.")
group.add_argument("--ozimux", action="store_true", default=False, help="Take payload input via OziMux (listen on port 8942).")
group.add_argument("--summary", action="store_true", default=False, help="Take payload input data via Payload Summary Broadcasts.")
parser.add_argument("--clamp", action="store_false", default=True, help="Clamp all tracks to ground.")
parser.add_argument("--nolabels", action="store_true", default=False, help="Inhibit labels on placemarks.")
parser.add_argument("--predict", action="store_true", help="Enable Flight Path Predictions.")
parser.add_argument("--predict_binary", type=str, default="./pred", help="Location of the CUSF predictor binary. Defaut = ./pred")
parser.add_argument("--burst_alt", type=float, default=30000.0, help="Expected Burst Altitude (m). Default = 30000")
parser.add_argument("--descent_rate", type=float, default=5.0, help="Expected Descent Rate (m/s, positive value). Default = 5.0")
parser.add_argument("--abort", action="store_true", default=False, help="Enable 'Abort' Predictions.")
parser.add_argument("--predict_rate", type=int, default=15, help="Run predictions every X seconds. Default = 15 seconds.")
parser.add_argument("-v", "--verbose", action="store_true", default=False, help="Verbose output.") parser.add_argument("-v", "--verbose", action="store_true", default=False, help="Verbose output.")
args = parser.parse_args() args = parser.parse_args()
@ -455,32 +468,58 @@ if __name__ == "__main__":
logging.getLogger('socketio').setLevel(logging.ERROR) logging.getLogger('socketio').setLevel(logging.ERROR)
logging.getLogger('engineio').setLevel(logging.ERROR) logging.getLogger('engineio').setLevel(logging.ERROR)
if args.ozimux: # Attempt to read in config file.
chasemapper_config = read_config(args.config)
# Die if we cannot read a valid config file.
if chasemapper_config == None:
logging.critical("Could not read any configuration data. Exiting")
sys.exit(1)
# Copy out the predictor settings to another dictionary.
pred_settings = {
'pred_binary': chasemapper_config['pred_binary'],
'gfs_path': chasemapper_config['pred_gfs_directory'],
'model_download': chasemapper_config['pred_model_download']
}
running_threads = []
# Start up the primary data source
if chasemapper_config['data_source'] == "ozimux":
logging.info("Using OziMux data source.") logging.info("Using OziMux data source.")
_listener = OziListener(telemetry_callback=ozi_listener_callback) _ozi_listener = OziListener(telemetry_callback=ozi_listener_callback, port=chasemapper_config['ozimux_port'])
running_threads.append(_ozi_listener)
# Start up UDP Broadcast Listener (which we use for car positions even if not for the payload) # Start up UDP Broadcast Listener (which we use for car positions even if not for the payload)
if args.summary: if (chasemapper_config['data_source'] == "horus_udp") or (chasemapper_config['car_gps_source'] == "horus_udp"):
logging.info("Using Payload Summary data source.") _horus_udp_port = chasemapper_config['horus_udp_port']
_broadcast_listener = UDPListener(summary_callback=udp_listener_summary_callback, if chasemapper_config['data_source'] == "horus_udp":
gps_callback=udp_listener_car_callback) _summary_callback = udp_listener_summary_callback
else: else:
_broadcast_listener = UDPListener(summary_callback=None, _summary_callback = None
gps_callback=udp_listener_car_callback)
_broadcast_listener.start() if chasemapper_config['car_gps_source'] == "horus_udp":
_gps_callback = udp_listener_car_callback
else:
_gps_callback = None
if args.predict: logging.info("Starting Horus UDP Listener")
_horus_udp_listener = UDPListener(summary_callback=_summary_callback,
gps_callback=_gps_callback)
_horus_udp_listener.start()
running_threads.append(_horus_udp_listener)
if chasemapper_config['pred_enabled']:
initPredictor() initPredictor()
# Run the Flask app, which will block until CTRL-C'd. # Run the Flask app, which will block until CTRL-C'd.
socketio.run(app, host='0.0.0.0', port=args.port) socketio.run(app, host=chasemapper_config['flask_host'], port=chasemapper_config['flask_port'])
# Attempt to close the listener. # Attempt to close the listeners.
try:
predictor_thread_running = False predictor_thread_running = False
_broadcast_listener.close() for _thread in running_threads:
_listener.close() try:
except: _thread.close()
pass except Exception as e:
logging.error("Error closing thread.")

Wyświetl plik

@ -475,6 +475,7 @@
var temp_data = {}; var temp_data = {};
temp_data.telem = data; temp_data.telem = data;
temp_data.path = [data.position]; temp_data.path = [data.position];
temp_data.burst = [];
temp_data.pred_path = []; temp_data.pred_path = [];
temp_data.pred_landing = []; temp_data.pred_landing = [];
temp_data.abort_path = []; temp_data.abort_path = [];
@ -550,21 +551,21 @@
// Add the landing marker if it doesnt exist. // Add the landing marker if it doesnt exist.
if (balloon_positions[_callsign].pred_marker == null){ 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]}) balloon_positions[_callsign].pred_marker = L.marker(data.pred_landing,{title:_callsign + " Landing", icon: balloonLandingIcons[balloon_positions[_callsign].colour]})
.bindTooltip(callsign + " Landing",{permanent:false,direction:'right'}) .bindTooltip(_callsign + " Landing",{permanent:false,direction:'right'})
.addTo(map); .addTo(map);
}else{ }else{
balloon_positions[callsign].pred_marker.setLatLng(data.pred_landing); balloon_positions[_callsign].pred_marker.setLatLng(data.pred_landing);
} }
if(data.burst.length == 3){ if(data.burst.length == 3){
// There is burst data! // There is burst data!
if (balloon_positions[_callsign].burst_marker == null){ if (balloon_positions[_callsign].burst_marker == null){
var _burst_txt = callsign + "Burst (" + data.burst[2].toFixed(0) + "m)"; var _burst_txt = _callsign + "Burst (" + data.burst[2].toFixed(0) + "m)";
balloon_positions[callsign].burst_marker = L.marker(data.burst,{title:_burst_txt, icon: burstIcon}) balloon_positions[_callsign].burst_marker = L.marker(data.burst,{title:_burst_txt, icon: burstIcon})
.bindTooltip(_burst_txt,{permanent:false,direction:'right'}) .bindTooltip(_burst_txt,{permanent:false,direction:'right'})
.addTo(map); .addTo(map);
}else{ }else{
balloon_positions[callsign].burst_marker.setLatLng(data.burst); balloon_positions[_callsign].burst_marker.setLatLng(data.burst);
} }
}else{ }else{
// No burst data, or we are in descent. // No burst data, or we are in descent.
@ -579,13 +580,13 @@
if (data.abort_landing.length == 3){ 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 (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{ }else{
balloon_positions[callsign].abort_marker.setLatLng(data.abort_landing); balloon_positions[_callsign].abort_marker.setLatLng(data.abort_landing);
} }
balloon_positions[_callsign].abort_path.setLatLngs(data.abort_path); balloon_positions[_callsign].abort_path.setLatLngs(data.abort_path);
@ -594,7 +595,7 @@
balloon_positions[_callsign].abort_path.setLatLngs([]); balloon_positions[_callsign].abort_path.setLatLngs([]);
if (balloon_positions[_callsign].abort_marker != null){ if (balloon_positions[_callsign].abort_marker != null){
balloon_positions[callsign].abort_marker.remove(); balloon_positions[_callsign].abort_marker.remove();
} }
} }
}); });