kopia lustrzana https://github.com/projecthorus/radiosonde_auto_rx
486 wiersze
23 KiB
HTML
486 wiersze
23 KiB
HTML
<!DOCTYPE HTML>
|
|
<html>
|
|
<head>
|
|
<title>Radiosonde Auto-RX Status</title>
|
|
|
|
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/c3.min.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/leaflet.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/tabulator_simple.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/autorx.css') }}" rel="stylesheet">
|
|
|
|
<!-- I should probably feel bad for using so many libraries, but apparently this is the way thing are done :-/ -->
|
|
<script src="{{ url_for('static', filename='js/jquery-3.3.1.min.js')}}"></script>
|
|
<script src="{{ url_for('static', filename='js/jquery-ui.min.js')}}"></script>
|
|
<script src="{{ url_for('static', filename='js/socket.io-1.4.5.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/leaflet.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/tabulator.min.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/d3.v5.min.js') }}" charset="utf-8"></script>
|
|
<script src="{{ url_for('static', filename='js/c3.min.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/scan_chart.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
|
|
|
<script type="text/javascript" charset="utf-8">
|
|
// Dummy auto_rx.config, which will be replaced with real data on page load.
|
|
var autorx_config = {
|
|
lat: 0.0,
|
|
lon: 0.0
|
|
};
|
|
|
|
// Object which will contain sonde markers and traces.
|
|
// properties for each key in this object (key = sonde ID)
|
|
// latest_data - latest sonde telemetry object from SondeDecoded
|
|
// marker: Leaflet marker object.
|
|
// path: Leaflet polyline object.
|
|
var sonde_positions = {};
|
|
|
|
// The sonde we are currently following on the map
|
|
var sonde_currently_following = "none";
|
|
|
|
$(document).ready(function() {
|
|
// Use the 'update_status' namespace for all of our traffic
|
|
namespace = '/update_status';
|
|
|
|
// Connect to the Socket.IO server.
|
|
// The connection URL has the following format:
|
|
// http[s]://<domain>:<port>[/<namespace>]
|
|
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
|
|
|
|
// Grab the System config on startup.
|
|
// Refer to config.py for the contents of the configuration blob.
|
|
$.ajax({
|
|
url: "/get_config",
|
|
dataType: 'json',
|
|
async: false,
|
|
success: function(data) {
|
|
autorx_config = data;
|
|
}
|
|
});
|
|
|
|
// Grab the version information
|
|
$.ajax({
|
|
url: "/get_version",
|
|
dataType: 'text',
|
|
async: true,
|
|
success: function(data) {
|
|
$('#autorx_header').html('<h1>Radiosonde Auto-RX Status <small> Version: ' + data + '</small></h1>');
|
|
}
|
|
});
|
|
|
|
|
|
// Event handler for Log data.
|
|
socket.on('log_event', function(msg) {
|
|
// On receipt, add to the log table.
|
|
var log_entry = "<tr><td class='col-2'>"+msg.timestamp+"</td><td class='col-1'>"+msg.level+"</td><td class='col-9'>"+msg.msg+"</td></tr>";
|
|
$('#log_data > tbody').prepend(log_entry);
|
|
// Scroll to the bottom of the log table
|
|
//$("#log_data > tbody").scrollTop($("#log_data > tbody")[0].scrollHeight);
|
|
});
|
|
|
|
// Instantiate the scan result chart (scan_chart.js)
|
|
setup_scan_chart();
|
|
|
|
// Scan Event handler
|
|
socket.on('scan_event', function(msg) {
|
|
// There is Scan data ready for us!
|
|
// Grab the latest set of data.
|
|
$.getJSON("/get_scan_data", function(data){
|
|
scan_chart_spectra.columns[0] = ['x_spectra'].concat(data.freq);
|
|
scan_chart_spectra.columns[1] = ['Spectra'].concat(data.power);
|
|
scan_chart_peaks.columns[0] = ['x_peaks'].concat(data.peak_freq);
|
|
scan_chart_peaks.columns[1] = ['Peaks'].concat(data.peak_lvl);
|
|
|
|
scan_chart_threshold.columns[1] = ['Threshold'].concat([data.threshold+autorx_config.snr_threshold,data.threshold+autorx_config.snr_threshold]);
|
|
// Plot the updated data.
|
|
scan_chart_obj.load(scan_chart_spectra);
|
|
scan_chart_obj.load(scan_chart_peaks);
|
|
scan_chart_obj.load(scan_chart_threshold);
|
|
|
|
// Show the latest scan time.
|
|
$('#scan_results').html('<b>Latest Scan:</b> ' + data.timestamp);
|
|
|
|
}
|
|
);
|
|
});
|
|
// Task Update event handler.
|
|
socket.on('task_event', function(msg){
|
|
// Grab the latest task list.
|
|
$.getJSON("/get_task_list", function(data){
|
|
var task_info = "";
|
|
for (_task in data){
|
|
task_info += "SDR #" + _task + ": " + data[_task] + " ";
|
|
}
|
|
|
|
$('#task_status').html("<h2>Current Task: <small>"+task_info+"</small></h2>");
|
|
});
|
|
});
|
|
|
|
// Sonde position Map
|
|
// Setup a basic Leaflet map, with an OSM tileset.
|
|
var sondemap = L.map('sonde_map').setView([autorx_config.station_lat, autorx_config.station_lon], 8);
|
|
|
|
// Add map layers
|
|
var osm_map = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
}).addTo(sondemap);
|
|
|
|
var esrimapLink =
|
|
'<a href="http://www.esri.com/">Esri</a>';
|
|
var esriwholink =
|
|
'i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community';
|
|
var esri_sat_map = L.tileLayer(
|
|
'http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
|
{
|
|
attribution: '© '+esrimapLink+', '+esriwholink,
|
|
maxZoom: 18,
|
|
});
|
|
sondemap.addControl(new L.Control.Layers({'OSM':osm_map, 'ESRI Satellite':esri_sat_map}));
|
|
|
|
// Home Icon.
|
|
var homeIcon = L.icon({
|
|
iconUrl: '{{ url_for('static', filename='img/antenna-green.png') }}',
|
|
iconSize: [26, 34],
|
|
iconAnchor: [13, 34]
|
|
});
|
|
|
|
// Add a new marker, but set it to hidden initially.
|
|
var home_marker = L.marker(sondemap.getCenter(),
|
|
{title: 'Receiver Location', icon: homeIcon}
|
|
).addTo(sondemap);
|
|
|
|
|
|
function mapMovedEvent(e){
|
|
// The user has panned the map, stop following things.
|
|
$("#sondeAutoFollow").prop('checked', false);
|
|
}
|
|
sondemap.on('dragend',mapMovedEvent);
|
|
|
|
|
|
// Telemetry Data Table
|
|
// Using tabulator makes this *really* easy.
|
|
$("#telem_table").tabulator({
|
|
height:180,
|
|
layout:"fitData",
|
|
layoutColumnsOnNewData:true,
|
|
columns:[ //Define Table Columns
|
|
{title:"SDR", field:"sdr_device_idx", headerSort:false},
|
|
{title:"Age", field:"age", headerSort:false},
|
|
{title:"Type", field:"type", headerSort:false},
|
|
{title:'Freq (MHz)', field:"freq", headerSort:false},
|
|
{title:"ID", field:"id", formatter:'html', headerSort:false},
|
|
{title:"Time", field:"datetime", headerSort:false},
|
|
{title:"Frame", field:"frame", headerSort:false},
|
|
{title:"Latitude", field:"lat", headerSort:false},
|
|
{title:"Longitude", field:"lon", headerSort:false},
|
|
{title:"Alt (m)", field:"alt", headerSort:false},
|
|
{title:"Asc (m/s)", field:"vel_v", headerSort:false},
|
|
{title:"Temp (°C)", field:"temp", headerSort:false},
|
|
{title:"Az (°)", field:"azimuth", headerSort:false},
|
|
{title:"El (°)", field:"elevation", headerSort:false},
|
|
{title:"Range (km)", field:"range", headerSort:false},
|
|
{title:"Other", field:"other", headerSort:false}
|
|
]
|
|
});
|
|
|
|
function updateTelemetryTable(){
|
|
var telem_data = [];
|
|
|
|
if (jQuery.isEmptyObject(sonde_positions)){
|
|
telem_data = [{sdr_device_idx:'None'}];
|
|
}else{
|
|
for (sonde_id in sonde_positions){
|
|
var sonde_id_data = Object.assign({},sonde_positions[sonde_id].latest_data);
|
|
var sonde_id_age = Date.now() - sonde_positions[sonde_id].age;
|
|
if (sonde_id_age>180000){
|
|
sonde_id_data.sdr_device_idx = "";
|
|
sonde_id_data.age = "old";
|
|
}else{
|
|
sonde_id_data.age = (sonde_id_age/1000.0).toFixed(0) + " s";
|
|
}
|
|
|
|
// If we have a station lat/lon/alt set, calculate az/el/range.
|
|
if (autorx_config.station_lat != 0.0){
|
|
// There is a station lat/lon set.
|
|
var _bal = {lat:sonde_id_data.lat, lon:sonde_id_data.lon, alt:sonde_id_data.alt};
|
|
var _station = {lat:autorx_config.station_lat, lon:autorx_config.station_lon, alt:autorx_config.station_alt};
|
|
|
|
var _look_angles = calculate_lookangles(_station, _bal);
|
|
|
|
sonde_id_data.azimuth = _look_angles.azimuth.toFixed(0);
|
|
sonde_id_data.elevation = _look_angles.elevation.toFixed(0);
|
|
sonde_id_data.range = (_look_angles.range/1000).toFixed(1);
|
|
} else{
|
|
// Insert blank data.
|
|
sonde_id_data.azimuth = "";
|
|
sonde_id_data.elevation = "";
|
|
sonde_id_data.range = "";
|
|
}
|
|
|
|
// Modify some of the fields to fixed point values.
|
|
sonde_id_data.lat = sonde_id_data.lat.toFixed(5);
|
|
sonde_id_data.lon = sonde_id_data.lon.toFixed(5);
|
|
sonde_id_data.alt = sonde_id_data.alt.toFixed(1);
|
|
sonde_id_data.vel_v = sonde_id_data.vel_v.toFixed(1);
|
|
// Add a link to HabHub if we have habitat enabled.
|
|
if ("habitat_enabled" in autorx_config){
|
|
if(autorx_config.habitat_enabled ==true){
|
|
sonde_id_data.id = "<a href='http://sondehub.org/" + sonde_id + "' target='_blank'>" + sonde_id + "</a>";
|
|
}
|
|
}
|
|
|
|
// Add data into the 'other' field.
|
|
sonde_id_data.other = "";
|
|
// Burst timer for RS41s
|
|
if (sonde_id_data.hasOwnProperty('bt')){
|
|
if ((sonde_id_data.bt >= 0) && (sonde_id_data.bt < 65535)) {
|
|
sonde_id_data.other += "BT " + new Date(sonde_id_data.bt*1000).toISOString().substr(11, 8) + " ";
|
|
}
|
|
}
|
|
|
|
telem_data.push(sonde_id_data);
|
|
}
|
|
}
|
|
|
|
$("#telem_table").tabulator("setData", telem_data);
|
|
}
|
|
|
|
// Deprecated. Left here for now in ase we need it later...
|
|
// function updateTelemetryText(){
|
|
// // Produce the text to go in the telemetry div.
|
|
// // Clear out the telemetry div ready for new data
|
|
// $('#telemetry').empty();
|
|
// // Generate the lines of telemetry information
|
|
// var telem_text = "";
|
|
// if (jQuery.isEmptyObject(sonde_positions)){
|
|
// telem_text = "No Sondes Tracked.";
|
|
// }else{
|
|
// for (sonde_id in sonde_positions){
|
|
// var msg = sonde_positions[sonde_id].latest_data;
|
|
// telem_text += '<b>Type: </b>' + msg.type + '<b> ID: </b>' + msg.id + ' <b>Timestamp: </b>' + msg.datetime + ' <b> Frame: </b> ' + msg.frame + '<b> Position: </b>' + msg.lat.toFixed(5) + ',' + msg.lon.toFixed(5) + '<b> Alt (m): </b>' + msg.alt.toFixed(1) + ' <b> Asc Rate (m/s): </b>' + msg.vel_v.toFixed(1) + ' <br>';
|
|
// }
|
|
// }
|
|
// // Update the div with the telemetry info.
|
|
// $('#telemetry').html(telem_text);
|
|
// }
|
|
|
|
|
|
|
|
// Grab the recent archive of telemetry data
|
|
// This should only ever be run once - on page load.
|
|
var initial_load_complete = false;
|
|
$.ajax({
|
|
url: "/get_telemetry_archive",
|
|
dataType: 'json',
|
|
async: true,
|
|
success: function(data) {
|
|
// Populate sonde_positions with data from the telemetry archive.
|
|
for (sonde_id in data){
|
|
var telem = data[sonde_id].latest_telem;
|
|
sonde_positions[sonde_id] = {
|
|
latest_data: telem,
|
|
age: 0,
|
|
colour: colour_values[colour_idx]
|
|
};
|
|
// Create markers
|
|
sonde_positions[sonde_id].path = L.polyline(data[sonde_id].path,{title:telem.id + " Path", color:sonde_positions[sonde_id].colour}).addTo(sondemap);
|
|
|
|
sonde_positions[sonde_id].marker = L.marker([telem.lat, telem.lon, telem.alt],{title:telem.id, icon: sondeAscentIcons[sonde_positions[sonde_id].colour]})
|
|
.bindTooltip(sonde_id,{permanent:false,direction:'right'})
|
|
.addTo(sondemap);
|
|
|
|
if(autorx_config.station_lat != 0.0){
|
|
sonde_positions[sonde_id].los_path = L.polyline([],
|
|
{
|
|
color:los_color,
|
|
opacity:los_opacity
|
|
}
|
|
).addTo(sondemap);
|
|
}
|
|
|
|
if (telem.vel_v < 0){
|
|
sonde_positions[sonde_id].marker.setIcon(sondeDescentIcons[sonde_positions[sonde_id].colour]);
|
|
}
|
|
|
|
colour_idx = (colour_idx+1)%colour_values.length;
|
|
}
|
|
// Update telemetry table
|
|
//updateTelemetryText();
|
|
updateTelemetryTable();
|
|
initial_load_complete = true;
|
|
}
|
|
});
|
|
|
|
|
|
// Telemetry event handler.
|
|
// We will get one of these with every line of telemetry decoded
|
|
socket.on('telemetry_event', function(msg) {
|
|
// Telemetry Event messages contain the entire telemetry dictionary, as produced by the SondeDecoder class.
|
|
// This includes the fields: ['frame', 'id', 'datetime', 'lat', 'lon', 'alt', 'temp', 'type', 'freq', 'freq_float']
|
|
|
|
if(initial_load_complete == false){
|
|
// If we have not completed our initial load of telemetry data, discard this data.
|
|
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.
|
|
sonde_positions[msg.id] = {
|
|
latest_data : msg,
|
|
age : Date.now(),
|
|
colour : colour_values[colour_idx]
|
|
};
|
|
// Create markers
|
|
sonde_positions[msg.id].path = L.polyline([[msg.lat, msg.lon, msg.alt]],{title:msg.id + " Path", color:sonde_positions[msg.id].colour}).addTo(sondemap);
|
|
|
|
sonde_positions[msg.id].marker = L.marker([msg.lat, msg.lon, msg.alt],{title:msg.id, icon: sondeAscentIcons[sonde_positions[msg.id].colour]})
|
|
.bindTooltip(msg.id,{permanent:false,direction:'right'})
|
|
.addTo(sondemap);
|
|
|
|
// If there is a station location defined, show the path from the station to the sonde.
|
|
if(autorx_config.station_lat != 0.0){
|
|
sonde_positions[msg.id].los_path = L.polyline([[autorx_config.station_lat, autorx_config.station_lon],[msg.lat, msg.lon]],
|
|
{
|
|
color:los_color,
|
|
opacity:los_opacity
|
|
}
|
|
).addTo(sondemap);
|
|
}
|
|
|
|
colour_idx = (colour_idx+1)%colour_values.length;
|
|
// If this is our first sonde since the browser has been opened, follow it.
|
|
if (Object.keys(sonde_positions).length == 1){
|
|
sonde_positions[msg.id].following = true;
|
|
}
|
|
} else {
|
|
// Yep - update the sonde_positions entry.
|
|
sonde_positions[msg.id].latest_data = msg;
|
|
sonde_positions[msg.id].age = Date.now();
|
|
sonde_positions[msg.id].path.addLatLng([msg.lat, msg.lon, msg.alt]);
|
|
sonde_positions[msg.id].marker.setLatLng([msg.lat, msg.lon, msg.alt]).update();
|
|
|
|
if (msg.vel_v < 0){
|
|
sonde_positions[msg.id].marker.setIcon(sondeDescentIcons[sonde_positions[msg.id].colour]);
|
|
}else{
|
|
sonde_positions[msg.id].marker.setIcon(sondeAscentIcons[sonde_positions[msg.id].colour]);
|
|
}
|
|
|
|
if(autorx_config.station_lat != 0.0){
|
|
sonde_positions[msg.id].los_path.setLatLngs([[autorx_config.station_lat, autorx_config.station_lon],[msg.lat, msg.lon]]);
|
|
}
|
|
}
|
|
|
|
// Update the telemetry table display
|
|
//updateTelemetryText();
|
|
updateTelemetryTable();
|
|
|
|
// Are we currently following any other sondes?
|
|
if (sonde_currently_following == "none"){
|
|
// If not, follow this one!
|
|
sonde_currently_following = msg.id;
|
|
}
|
|
|
|
// Is sonde following enabled?
|
|
if (document.getElementById("sondeAutoFollow").checked == true){
|
|
// If we are currently following this sonde, snap the map to it.
|
|
if (msg.id == sonde_currently_following){
|
|
sondemap.panTo([msg.lat,msg.lon]);
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// Sonde-Following Logic. May need to adjust timeouts.
|
|
var sonde_follow_timeout = 30000; // 30 Seconds - reasonable timeout.
|
|
// Every X seconds, check if the currently followed sonde is still getting regular data.
|
|
// If not, clear the currently_following flag to allow another sonde to be auto tracked.
|
|
window.setInterval(function () {
|
|
if (sonde_currently_following == "none"){
|
|
return;
|
|
}
|
|
var now_time = Date.now();
|
|
if ( (now_time-sonde_positions[sonde_currently_following].age) > sonde_follow_timeout){
|
|
sonde_currently_following = "none";
|
|
}
|
|
}, sonde_follow_timeout);
|
|
|
|
|
|
// Update telemetry table every 5 seconds (this is mainly to update the age field)
|
|
window.setInterval(function(){
|
|
updateTelemetryTable();
|
|
}, 5000);
|
|
|
|
|
|
// Tell the server we are connected and ready for data.
|
|
socket.on('connect', function() {
|
|
socket.emit('client_connected', {data: 'I\'m connected!'});
|
|
// This will cause the server to emit a few messages telling us to fetch data.
|
|
});
|
|
|
|
// Callback function for Hide Scan Plot checkbox.
|
|
$( "#hideScanPlot" ).click(function() {
|
|
if (document.getElementById("hideScanPlot").checked == true){
|
|
$("#scan_chart").hide("fast");
|
|
} else {
|
|
$("#scan_chart").show("fast");
|
|
}
|
|
});
|
|
|
|
// Callback function for Hide Map checkbox.
|
|
$( "#hideMap" ).click(function() {
|
|
if (document.getElementById("hideMap").checked == true){
|
|
$("#sonde_map").hide("fast");
|
|
} else {
|
|
$("#sonde_map").show("fast");
|
|
}
|
|
});
|
|
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="autorx_header"><h1>Radiosonde Auto-RX Status</h1></div>
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class='col-12'>
|
|
<div id="task_status"><h2>Current Task:</h2></div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class='col-12' id="telem_table"></div>
|
|
</div>
|
|
<div class="row">
|
|
<div class='col-12'>
|
|
Auto-Follow Latest Sonde: <input type="checkbox" id="sondeAutoFollow" checked> Hide Map: <input type="checkbox" id="hideMap">
|
|
<div id="sonde_map" style="height:400px;width:100%"></div>
|
|
<br>
|
|
</div>
|
|
</div>
|
|
<div class="row" id='scan_column'>
|
|
<div class='col-12'>
|
|
<h2>Scan Results:</h2>
|
|
<div id='scan_results'>No scan data yet...</div>
|
|
Hide Scan Plot: <input type="checkbox" id="hideScanPlot">
|
|
<div id="scan_chart" style="width:100%;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class='col-12'>
|
|
<h2>Log:</h2>
|
|
<table id="log_data" class="table table-fixed">
|
|
<thead>
|
|
<tr>
|
|
<th class="col-2">Time</th>
|
|
<th class="col-1">Level</th>
|
|
<th class="col-9">Message</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|