kopia lustrzana https://github.com/projecthorus/radiosonde_auto_rx
				
				
				
			Add internal calculation of ascent/descent rates, using GenericTrack class from chasemapper.
							rodzic
							
								
									16e97af089
								
							
						
					
					
						commit
						c6915a7196
					
				|  | @ -0,0 +1,258 @@ | |||
| #!/usr/bin/env python | ||||
| # | ||||
| #   Project Horus - Flight Data to Geometry | ||||
| # | ||||
| #   Copyright (C) 2018  Mark Jessop <vk5qi@rfhead.net> | ||||
| #   Released under GNU GPL v3 or later | ||||
| # | ||||
| import math | ||||
| import traceback | ||||
| import logging | ||||
| import numpy as np | ||||
| from .utils import position_info | ||||
| 
 | ||||
| 
 | ||||
| def getDensity(altitude): | ||||
|     '''  | ||||
|     Calculate the atmospheric density for a given altitude in metres. | ||||
|     This is a direct port of the oziplotter Atmosphere class | ||||
|     ''' | ||||
| 
 | ||||
|     #Constants | ||||
|     airMolWeight = 28.9644  # Molecular weight of air | ||||
|     densitySL = 1.225   # Density at sea level [kg/m3] | ||||
|     pressureSL = 101325     # Pressure at sea level [Pa] | ||||
|     temperatureSL = 288.15  # Temperature at sea level [deg K] | ||||
|     gamma = 1.4 | ||||
|     gravity = 9.80665   # Acceleration of gravity [m/s2] | ||||
|     tempGrad = -0.0065  # Temperature gradient [deg K/m] | ||||
|     RGas = 8.31432  # Gas constant [kg/Mol/K] | ||||
|     R = 287.053   | ||||
|     deltaTemperature = 0.0;  | ||||
| 
 | ||||
|     # Lookup Tables | ||||
|     altitudes = [0, 11000, 20000, 32000, 47000, 51000, 71000, 84852] | ||||
|     pressureRels = [1, 2.23361105092158e-1, 5.403295010784876e-2, 8.566678359291667e-3, 1.0945601337771144e-3, 6.606353132858367e-4, 3.904683373343926e-5, 3.6850095235747942e-6] | ||||
|     temperatures = [288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65, 186.946] | ||||
|     tempGrads = [-6.5, 0, 1, 2.8, 0, -2.8, -2, 0] | ||||
|     gMR = gravity * airMolWeight / RGas; | ||||
| 
 | ||||
|     # Pick a region to work in | ||||
|     i = 0 | ||||
|     if(altitude > 0): | ||||
|         while (altitude > altitudes[i+1]): | ||||
|             i = i + 1 | ||||
|      | ||||
| 
 | ||||
|     # Lookup based on region | ||||
|     baseTemp = temperatures[i] | ||||
|     tempGrad = tempGrads[i] / 1000.0 | ||||
|     pressureRelBase = pressureRels[i] | ||||
|     deltaAltitude = altitude - altitudes[i] | ||||
|     temperature = baseTemp + tempGrad * deltaAltitude | ||||
| 
 | ||||
|     # Calculate relative pressure | ||||
|     if(math.fabs(tempGrad) < 1e-10): | ||||
|         pressureRel = pressureRelBase * math.exp(-1 *gMR * deltaAltitude / 1000.0 / baseTemp) | ||||
|     else: | ||||
|         pressureRel = pressureRelBase * math.pow(baseTemp / temperature, gMR / tempGrad / 1000.0) | ||||
|      | ||||
| 
 | ||||
|     # Add temperature offset | ||||
|     temperature = temperature + deltaTemperature | ||||
| 
 | ||||
|     # Finally, work out the density... | ||||
|     speedOfSound = math.sqrt(gamma * R * temperature) | ||||
|     pressure = pressureRel * pressureSL | ||||
|     density = densitySL * pressureRel * temperatureSL / temperature | ||||
| 
 | ||||
|     return density | ||||
| 
 | ||||
| 
 | ||||
| def seaLevelDescentRate(descent_rate, altitude): | ||||
|     ''' Calculate the descent rate at sea level, for a given descent rate at altitude ''' | ||||
| 
 | ||||
|     rho = getDensity(altitude) | ||||
|     return math.sqrt((rho / 1.22) * math.pow(descent_rate, 2)) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| def time_to_landing(current_altitude, current_descent_rate=-5.0, ground_asl=0.0, step_size=1): | ||||
|     ''' Calculate an estimated time to landing (in seconds) of a payload, based on its current altitude and descent rate ''' | ||||
| 
 | ||||
|     # A few checks on the input data. | ||||
|     if current_descent_rate > 0.0: | ||||
|         # If we are still ascending, return none. | ||||
|         return None | ||||
| 
 | ||||
|     if current_altitude <= ground_asl: | ||||
|         # If the current altitude is *below* ground level, we have landed. | ||||
|         return 0 | ||||
| 
 | ||||
|     # Calculate the sea level descent rate. | ||||
|     _desc_rate = math.fabs(seaLevelDescentRate(current_descent_rate, current_altitude)) | ||||
|     _drag_coeff = _desc_rate*1.1045 # Magic multiplier from predict.php | ||||
| 
 | ||||
| 
 | ||||
|     _alt = current_altitude | ||||
|     _start_time = 0 | ||||
|     # Now step through the flight in <step_size> second steps. | ||||
|     # Once the altitude is below our ground level, stop, and return the elapsed time. | ||||
|     while _alt >= ground_asl: | ||||
|         _alt += step_size * -1*(_drag_coeff/math.sqrt(getDensity(_alt))) | ||||
|         _start_time += step_size | ||||
| 
 | ||||
| 
 | ||||
|     return _start_time | ||||
| 
 | ||||
| class GenericTrack(object): | ||||
|     """ | ||||
|     A Generic 'track' object, which stores track positions for a payload or chase car. | ||||
|     Telemetry is added using the add_telemetry method, which takes a dictionary with time/lat/lon/alt keys (at minimum). | ||||
|     This object performs a running average of the ascent/descent rate, and calculates the predicted landing rate if the payload | ||||
|     is in descent. | ||||
|     The track history can be exported to a LineString using the to_line_string method. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, | ||||
|         ascent_averaging = 6, | ||||
|         landing_rate = 5.0): | ||||
|         ''' Create a GenericTrack Object. ''' | ||||
| 
 | ||||
|         # Averaging rate. | ||||
|         self.ASCENT_AVERAGING = ascent_averaging | ||||
|         # Payload state. | ||||
|         self.landing_rate = landing_rate | ||||
|         self.ascent_rate = 0.0 | ||||
|         self.heading = 0.0 | ||||
|         self.speed = 0.0 | ||||
|         self.is_descending = False | ||||
| 
 | ||||
|         # Internal store of track history data. | ||||
|         # Data is stored as a list-of-lists, with elements of [datetime, lat, lon, alt, comment] | ||||
|         self.track_history = [] | ||||
| 
 | ||||
| 
 | ||||
|     def add_telemetry(self,data_dict): | ||||
|         '''  | ||||
|         Accept telemetry data as a dictionary with fields  | ||||
|         datetime, lat, lon, alt, comment | ||||
|         ''' | ||||
| 
 | ||||
|         try: | ||||
|             _datetime = data_dict['time'] | ||||
|             _lat = data_dict['lat'] | ||||
|             _lon = data_dict['lon'] | ||||
|             _alt = data_dict['alt'] | ||||
|             if 'comment' in data_dict.keys(): | ||||
|                 _comment = data_dict['comment'] | ||||
|             else: | ||||
|                 _comment = "" | ||||
| 
 | ||||
|             self.track_history.append([_datetime, _lat, _lon, _alt, _comment]) | ||||
|             self.update_states() | ||||
|             return self.get_latest_state() | ||||
|         except: | ||||
|             #logging.error("Web - Error adding new telemetry to GenericTrack %s" % traceback.format_exc()) | ||||
|             pass | ||||
| 
 | ||||
| 
 | ||||
|     def get_latest_state(self): | ||||
|         ''' Get the latest position of the payload ''' | ||||
| 
 | ||||
|         if len(self.track_history) == 0: | ||||
|             return None | ||||
|         else: | ||||
|             _latest_position = self.track_history[-1] | ||||
|             _state = { | ||||
|                 'time'  : _latest_position[0], | ||||
|                 'lat'   : _latest_position[1], | ||||
|                 'lon'   : _latest_position[2], | ||||
|                 'alt'   : _latest_position[3], | ||||
|                 'ascent_rate': self.ascent_rate, | ||||
|                 'is_descending': self.is_descending, | ||||
|                 'landing_rate': self.landing_rate, | ||||
|                 'heading': self.heading, | ||||
|                 'speed': self.speed | ||||
|             } | ||||
|             return _state | ||||
| 
 | ||||
| 
 | ||||
|     def calculate_ascent_rate(self): | ||||
|         ''' Calculate the ascent/descent rate of the payload based on the available data ''' | ||||
|         if len(self.track_history) <= 1: | ||||
|             return 0.0 | ||||
|         elif len(self.track_history) == 2: | ||||
|             # Basic ascent rate case - only 2 samples. | ||||
|             _time_delta = (self.track_history[-1][0] - self.track_history[-2][0]).total_seconds() | ||||
|             _altitude_delta = self.track_history[-1][3] - self.track_history[-2][3] | ||||
| 
 | ||||
|             return _altitude_delta/_time_delta | ||||
| 
 | ||||
| 
 | ||||
|         else: | ||||
|             _num_samples = min(len(self.track_history), self.ASCENT_AVERAGING) | ||||
|             _asc_rates = [] | ||||
| 
 | ||||
|             for _i in range(-1*(_num_samples-1), 0): | ||||
|                 _time_delta = (self.track_history[_i][0] - self.track_history[_i-1][0]).total_seconds() | ||||
|                 _altitude_delta = self.track_history[_i][3] - self.track_history[_i-1][3] | ||||
|                 _asc_rates.append(_altitude_delta/_time_delta) | ||||
| 
 | ||||
|             return np.mean(_asc_rates) | ||||
| 
 | ||||
|     def calculate_heading(self): | ||||
|         ''' Calculate the heading of the payload ''' | ||||
|         if len(self.track_history) <= 1: | ||||
|             return 0.0 | ||||
|         else: | ||||
|             _pos_1 = self.track_history[-2] | ||||
|             _pos_2 = self.track_history[-1] | ||||
| 
 | ||||
|             _pos_info = position_info((_pos_1[1],_pos_1[2],_pos_1[3]), (_pos_2[1],_pos_2[2],_pos_2[3])) | ||||
| 
 | ||||
|             return _pos_info['bearing'] | ||||
| 
 | ||||
|     def calculate_speed(self): | ||||
|         """ Calculate Payload Speed in metres per second """ | ||||
|         if len(self.track_history)<=1: | ||||
|             return 0.0 | ||||
|         else: | ||||
|             _time_delta = (self.track_history[-1][0] - self.track_history[-2][0]).total_seconds() | ||||
|             _pos_1 = self.track_history[-2] | ||||
|             _pos_2 = self.track_history[-1] | ||||
| 
 | ||||
|             _pos_info = position_info((_pos_1[1],_pos_1[2],_pos_1[3]), (_pos_2[1],_pos_2[2],_pos_2[3])) | ||||
| 
 | ||||
|             _speed = _pos_info['great_circle_distance']/_time_delta | ||||
| 
 | ||||
|             return _speed | ||||
| 
 | ||||
| 
 | ||||
|     def update_states(self): | ||||
|         ''' Update internal states based on the current data ''' | ||||
|         self.ascent_rate = self.calculate_ascent_rate() | ||||
|         self.heading = self.calculate_heading() | ||||
|         self.speed = self.calculate_speed() | ||||
|         self.is_descending = self.ascent_rate < 0.0 | ||||
| 
 | ||||
|         if self.is_descending: | ||||
|             _current_alt = self.track_history[-1][3] | ||||
|             self.landing_rate = seaLevelDescentRate(self.ascent_rate, _current_alt) | ||||
| 
 | ||||
| 
 | ||||
|     def to_polyline(self): | ||||
|         ''' Generate and return a Leaflet PolyLine compatible array ''' | ||||
|         # Copy array into a numpy representation for easier slicing. | ||||
|         if len(self.track_history) == 0: | ||||
|             return [] | ||||
|         elif len(self.track_history) == 1: | ||||
|             # LineStrings need at least 2 points. If we only have a single point, | ||||
|             # fudge it by duplicating the single point. | ||||
|             _track_data_np = np.array([self.track_history[0], self.track_history[0]]) | ||||
|         else: | ||||
|             _track_data_np = np.array(self.track_history) | ||||
|         # Produce new array | ||||
|         _track_points = np.column_stack((_track_data_np[:,1], _track_data_np[:,2], _track_data_np[:,3])) | ||||
| 
 | ||||
|         return _track_points.tolist() | ||||
|  | @ -326,6 +326,7 @@ | |||
|                     return | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 // Have we seen this sonde before?  | ||||
|                 if (sonde_positions.hasOwnProperty(msg.id) == false){ | ||||
|                     // Nope, add a property to the sonde_positions object, and setup markers for the sonde. | ||||
|  |  | |||
|  | @ -17,6 +17,8 @@ import subprocess | |||
| import threading | ||||
| import time | ||||
| import numpy as np | ||||
| from dateutil.parser import parse | ||||
| from datetime import datetime, timedelta | ||||
| from math import radians, degrees, sin, cos, atan2, sqrt, pi | ||||
| from . import __version__ as auto_rx_version | ||||
| try: | ||||
|  | @ -738,7 +740,6 @@ def peak_decimation(freq, power, factor): | |||
|     return (_freq_out, _power_out) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     import sys | ||||
|     logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG) | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| #   Copyright (C) 2018  Mark Jessop <vk5qi@rfhead.net> | ||||
| #   Released under MIT License | ||||
| # | ||||
| import copy | ||||
| import datetime | ||||
| import json | ||||
| import logging | ||||
|  | @ -15,6 +16,7 @@ import traceback | |||
| import autorx | ||||
| import autorx.config | ||||
| import autorx.scan | ||||
| from autorx.geometry import GenericTrack | ||||
| from threading import Thread | ||||
| import flask | ||||
| from flask import request, abort | ||||
|  | @ -45,6 +47,7 @@ socketio = SocketIO(app, async_mode='threading') | |||
| #   'latest_timestamp': timestamp (unix timestamp) of when the last packet was received. | ||||
| #   'latest_telem': telemetry dictionary. | ||||
| #   'path': list of [lat,lon,alt] pairs | ||||
| #   'track': A GenericTrack object, which is used to determine the current ascent/descent rate. | ||||
| # | ||||
| flask_telemetry_store = {} | ||||
| 
 | ||||
|  | @ -117,7 +120,12 @@ def flask_get_scan_data(): | |||
| @app.route("/get_telemetry_archive") | ||||
| def flask_get_telemetry_archive(): | ||||
|     """ Return a copy of the telemetry archive """ | ||||
|     return json.dumps(flask_telemetry_store) | ||||
|     # Make a copy of the store, and remove the non-serialisable GenericTrack object | ||||
|     _temp_store = copy.deepcopy(flask_telemetry_store) | ||||
|     for _element in _temp_store: | ||||
|         _temp_store[_element].pop('track') | ||||
| 
 | ||||
|     return json.dumps(_temp_store) | ||||
| 
 | ||||
| 
 | ||||
| @app.route("/shutdown/<shutdown_key>") | ||||
|  | @ -292,21 +300,32 @@ class WebExporter(object): | |||
|                 return | ||||
|          | ||||
|         _telem = telemetry.copy() | ||||
| 
 | ||||
|         # Add the telemetry information to the global telemetry store | ||||
|         if _telem['id'] not in flask_telemetry_store: | ||||
|             flask_telemetry_store[_telem['id']] = {'timestamp':time.time(), 'latest_telem':_telem, 'path':[], 'track': GenericTrack()} | ||||
| 
 | ||||
|         flask_telemetry_store[_telem['id']]['path'].append([_telem['lat'],_telem['lon'],_telem['alt']]) | ||||
|         flask_telemetry_store[_telem['id']]['latest_telem'] = _telem | ||||
|         flask_telemetry_store[_telem['id']]['timestamp'] = time.time() | ||||
| 
 | ||||
|         # Update the sonde's track and extract the current state. | ||||
|         flask_telemetry_store[_telem['id']]['track'].add_telemetry({'time': _telem['datetime_dt'], 'lat':_telem['lat'], 'lon': _telem['lon'], 'alt':_telem['alt']}) | ||||
|         _telem_state = flask_telemetry_store[_telem['id']]['track'].get_latest_state()    | ||||
| 
 | ||||
|         # Add the calculated vertical and horizontal velocity, and heading to the telemetry dict. | ||||
|         _telem['vel_v'] = _telem_state['ascent_rate'] | ||||
|         _telem['vel_h'] = _telem_state['speed'] | ||||
|         _telem['heading'] = _telem_state['heading'] | ||||
| 
 | ||||
|         # Remove the datetime object that is part of the telemetry, if it exists. | ||||
|         # (it might not be present in test data) | ||||
|         if 'datetime_dt' in _telem: | ||||
|             _telem.pop('datetime_dt') | ||||
| 
 | ||||
|         # Pass it on to the client. | ||||
|         socketio.emit('telemetry_event', _telem, namespace='/update_status') | ||||
| 
 | ||||
|         # Add the telemetry information to the global telemetry store | ||||
|         if _telem['id'] not in flask_telemetry_store: | ||||
|             flask_telemetry_store[_telem['id']] = {'timestamp':time.time(), 'latest_telem':_telem, 'path':[]} | ||||
| 
 | ||||
|         flask_telemetry_store[_telem['id']]['path'].append([_telem['lat'],_telem['lon'],_telem['alt']]) | ||||
|         flask_telemetry_store[_telem['id']]['latest_telem'] = _telem | ||||
|         flask_telemetry_store[_telem['id']]['timestamp'] = time.time() | ||||
| 
 | ||||
| 
 | ||||
|     def clean_telemetry_store(self): | ||||
|         """ Remove any old data from the telemetry store """ | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Mark Jessop
						Mark Jessop