diff --git a/RX_FSK/RX_FSK.ino b/RX_FSK/RX_FSK.ino old mode 100644 new mode 100755 index e75d101..5e3d790 --- a/RX_FSK/RX_FSK.ino +++ b/RX_FSK/RX_FSK.ino @@ -99,6 +99,8 @@ static unsigned long specTimer; void enterMode(int mode); void WiFiEvent(WiFiEvent_t event); +char buffer[85]; +MicroNMEA nmea(buffer, sizeof(buffer)); // Read line from file, independent of line termination (LF or CR LF) String readLine(Stream &stream) { @@ -515,6 +517,39 @@ const char *createStatusForm() { return message; } +const char *createLiveJson() { + char *ptr = message; + strcpy(ptr, "{"); + + SondeInfo *s = &sonde.sondeList[sonde.currentSonde]; + if (s->validID) { + sprintf(ptr + strlen(ptr), "\"sonde\": {\"id\": \"%s\", \"freq\": %3.3f, \"type\": \"%s\", \"lat\": %.6f, \"lon\": %.6f, \"alt\": %.0f, \"speed\": %.1f, \"dir\": %.0f, \"climb\": %.1f }", s->id, s->freq, sondeTypeStr[s->type], s->lat, s->lon, s->alt, s->hs, s->dir, s->vs); + } else { + sprintf(ptr + strlen(ptr), "\"sonde\": {\"launchsite\": \"%s\",\"freq\": %3.3f, \"type\": \"%s\" }", s->launchsite, s->freq, sondeTypeStr[s->type]); + } + + if (sonde.config.gps_rxd < 0) { + // gps disabled + } else { + long sat = nmea.getNumSatellites(); + long speed = nmea.getSpeed(); + long dir = nmea.getCourse(); + long lat = nmea.getLatitude(); + long lon = nmea.getLongitude(); + long alt = -1; + bool b = nmea.getAltitude(alt); + bool valid = nmea.isValid(); + uint8_t hdop = nmea.getHDOP(); + if (valid) { + strcat(ptr, ","); + sprintf(ptr + strlen(ptr), "\"gps\": {\"lat\": %ld, \"lon\": %ld, \"alt\": %ld, \"sat\": %ld, \"speed\": %ld, \"dir\": %ld, \"hdop\": %d }", lat, lon, alt, sat, speed, dir, hdop); + } + + } + + strcat(ptr, "}"); + return message; +} ///////////////////// Config form @@ -1175,6 +1210,15 @@ void SetupAsyncServer() { server.on("/status.html", HTTP_GET, [](AsyncWebServerRequest * request) { request->send(200, "text/html", createStatusForm()); }); + server.on("/live.json", HTTP_GET, [](AsyncWebServerRequest * request) { + request->send(200, "text/json", createLiveJson()); + }); + server.on("/livemap.html", HTTP_GET, [](AsyncWebServerRequest * request) { + request->send(SPIFFS, "/livemap.html", String(), false, processor); + }); + server.on("/livemap.js", HTTP_GET, [](AsyncWebServerRequest * request) { + request->send(SPIFFS, "/livemap.js", String(), false, processor); + }); server.on("/update.html", HTTP_GET, [](AsyncWebServerRequest * request) { request->send(200, "text/html", createUpdateForm(0)); }); @@ -1370,8 +1414,6 @@ void initTouch() { } } -char buffer[85]; -MicroNMEA nmea(buffer, sizeof(buffer)); diff --git a/RX_FSK/data/index.html b/RX_FSK/data/index.html old mode 100644 new mode 100755 index 036eb00..6b4b088 --- a/RX_FSK/data/index.html +++ b/RX_FSK/data/index.html @@ -15,6 +15,7 @@ + diff --git a/RX_FSK/data/livemap.html b/RX_FSK/data/livemap.html new file mode 100644 index 0000000..779e025 --- /dev/null +++ b/RX_FSK/data/livemap.html @@ -0,0 +1,17 @@ + + + + rdzTTGOSonde Server LiveMap + + + + + + + + + + +
+ + diff --git a/RX_FSK/data/livemap.js b/RX_FSK/data/livemap.js new file mode 100644 index 0000000..a1afe01 --- /dev/null +++ b/RX_FSK/data/livemap.js @@ -0,0 +1,396 @@ +$(document).ready(function(){ + + var map = L.map('map', { attributionControl: false }); + map.on('mousedown touchstart',function () { follow=false; }); + + L.control.scale().addTo(map); + L.control.attribution({prefix:false}).addTo(map); + + var osm = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', { + attribution: '
Leaflet · Map: OpenStreetMap
', + minZoom: 1, + maxZoom: 19 + }); + var esri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '
Leaflet · Map: Esri · Earthstar Geographics
', + minZoom: 1, + maxZoom: 20 + }); + + var basemap = 'osm'; + osm.addTo(map); + + basemap_change = function () { + if (basemap == 'osm') { + map.removeLayer(osm); + map.addLayer(esri); + basemap = 'esri'; + } else { + map.removeLayer(esri); + map.addLayer(osm); + basemap = 'osm'; + } + }; + + map.setView([51.163361,10.447683], 5); // Mitte DE + +$('#map .leaflet-control-container').append(L.DomUtil.create('div', 'leaflet-top leaflet-center leaflet-header')); +var header = ''; +header += '
rdzTTGOSonde LiveMap
🎈 - MHz -
'; +header += '
m | m/s | km/h
'; +header += '
'; +$('.leaflet-header').append(header); + +$('#map .leaflet-control-container').append(L.DomUtil.create('div', 'leaflet-bottom leaflet-center leaflet-footer')); +var footer = ''; +footer += '
Direction: ...Β°
Distance: ...m
'; +$('.leaflet-footer').append(footer); + +var statbar = ''; +headtxt = function(data,stat) { + var staticon = (stat == '1')?'🟒':'🟑'; + statbar = staticon + statbar; + if ((statbar.length) > 20) { statbar = statbar.substring(0,20); } + if (data.lat == '0.000000') { return false; } + if (data.id) { + $('#sonde_id').html(data.id); + $('#sonde_alt').html(data.alt); + $('#sonde_climb').html(data.climb); + $('#sonde_speed').html( mr(data.speed * 3.6 * 10) / 10 ); + $('#sonde_detail').show(); + } else { + $('#sonde_id').html(data.launchsite.trim()); + $('#sonde_detail').hide(); + } + $('#sonde_freq').html(data.freq); + $('#sonde_type').html(data.type); + $('#sonde_statbar').html(statbar); +}; + + map.addControl(new L.Control.Button([{ position: 'topleft', text: 'πŸ—ΊοΈ', href: 'javascript:basemap_change();' }])); + + map.addControl(new L.Control.Button([ + { position: 'topright', id: "status", text: 'πŸ”΄', href: 'javascript:get_data();' }, + { text: 'βš™οΈ', href: 'index.html' } + ])); + + map.addControl(new L.Control.Button([ + { position:'topright', text: '🎈', href: 'javascript:show(marker,\'marker\');' }, + { text: '〰️', href: 'javascript:show_line();' }, + { text: 'πŸ’₯', href: 'javascript:show(marker_burst,\'burst\');' }, + { text: '🎯', href: 'javascript:show(marker_landing,\'landing\');' } + ])); + + + show = function(e,p) { + if (p == 'landing') { get_predict(last_data); } + if (e) { + map.closePopup(); + map.setView(map._layers[e._leaflet_id].getLatLng()); + map._layers[e._leaflet_id].openPopup(); + follow = p; + } + }; + + + getTwoBounds = function (a,b) { + var sW = new L.LatLng((a._southWest.lat > b._southWest.lat)?b._southWest.lat:a._southWest.lat, (a._southWest.lng > b._southWest.lng)?b._southWest.lng:a._southWest.lng); + var nE = new L.LatLng((a._northEast.lat < b._northEast.lat)?b._northEast.lat:a._northEast.lat, (a._northEast.lng < b._northEast.lng)?b._northEast.lng:a._northEast.lng); + + return new L.LatLngBounds(sW, nE); + }; + + show_line = function() { + $('.i_position, .i_landing').remove(); + map.closePopup(); + if (line._latlngs.length != 0 && line_predict._latlngs.length != 0) { + map.fitBounds(getTwoBounds(line.getBounds(),line_predict.getBounds())); + } else if (line._latlngs.length != 0) { + map.fitBounds(line.getBounds()); + } else if (line_predict._latlngs.length != 0) { + map.fitBounds(line_predict.getBounds()); + } + }; + + + + last_data = false; + follow = 'marker'; + + marker_landing = false; + icon_landing = L.divIcon({className: 'leaflet-landing'}); + dots_predict = []; + line_predict = L.polyline(dots_predict,{color: 'yellow'}).addTo(map); + marker_burst = false; + icon_burst = L.divIcon({className: 'leaflet-burst'}); + + marker = false; + dots = []; + line = L.polyline(dots).addTo(map); + + draw = function(data) { + var stat; + if (data.id) { + + if ((data.lat != '0.000000' && data.lon != '0.000000') && (JSON.stringify(data) != JSON.stringify(last_data)) ) { + var location = [data.lat,data.lon,data.alt]; + if (!marker) { + map.setView(location, 14); + marker = L.marker(location).addTo(map) + .bindPopup(poptxt('position',data),{closeOnClick:false, autoPan:false}).openPopup(); + get_predict(data); + } else { + marker.slideTo(location, { + duration: 500, + keepAtCenter: (follow=='marker')?true:false + }) + .setPopupContent(poptxt('position',data)); + if (last_data.id != data.id) { + storage_remove(); + dots = []; + get_predict(data); + } + } + dots.push(location); + line.setLatLngs(dots); + storage_write(data); + $('#status').html('🟒'); + stat = 1; + } else { + $('#status').html('🟑'); + stat = 0; + } + headtxt(data,stat); + last_data = data; + } else { + $('#status').html('🟑'); + headtxt(data,0); + } + }; + + + marker_gps = false; + icon_gps = L.divIcon({className: 'leaflet-gps'}); + circ_gps = false; + + gps = function(e) { + gps_location = [e.lat/1000000,e.lon/1000000]; + gps_accuracy = e.hdop*2; + + if (last_data && last_data.lat != '0.000000') { + if ($('.leaflet-footer').css('display') == 'none') { $('.leaflet-footer').show(); } + + var distance = Math.round(map.distance(gps_location,[last_data.lat, last_data.lon])); + distance = (distance > 1000)?(distance / 1000) + 'k':distance; + $('.leaflet-footer .gps_dist').html(distance); + + $('.leaflet-footer .gps_dir').html( bearing(gps_location,[last_data.lat, last_data.lon]) ); + } + + if (!marker_gps) { + map.addControl(new L.Control.Button([{ position: 'topleft', text: 'πŸ›°οΈ', href: 'javascript:show(marker_gps,\'gps\');' }])); + + marker_gps = L.marker(gps_location,{icon:icon_gps}).addTo(map) + .bindPopup(poptxt('gps',e),{closeOnClick:false, autoPan:false}); + circ_gps = L.circle(gps_location, gps_accuracy).addTo(map); + } else { + marker_gps.slideTo(gps_location, { + duration: 500, + keepAtCenter: (follow=='gps')?true:false + }) + .setPopupContent(poptxt('gps',e)); + circ_gps.slideTo(gps_location, { duration: 500 }); + circ_gps.setRadius(gps_accuracy); + } + }; + + get_data = function() { + $('#status').html('πŸ”΄'); + $.get('live.json', function( data ) { + if (typeof data != "object") { data = $.parseJSON(data);Β } + if (data.sonde) { + draw(data.sonde); + } else { + setTimeout(function() {$('#status').html('🟑');},100); + } + if (data.gps) { + gps(data.gps); + } + }); + }; + + predictor = false; + get_predict = function(data) { + if (!data) { return; } + var ascent = (data.climb > 0)? data.climb : 15; + var descent = (data.climb > 0)? 5 : data.climb * -1; + + var burst; + if (data.climb > 0) { + burst = (data.alt > 32500)?data.alt + 500 : 32500; + } else { + burst = parseInt(data.alt) + 7; + if (data.alt > 12000) { descent = 6; } + } + + var m = new Date(); + var datetime = m.getUTCFullYear() + "-" + az(m.getUTCMonth()+1) + "-" + az(m.getUTCDate()) + "T" + + az(m.getUTCHours()) + ":" + az(m.getUTCMinutes()) + ":" + az(m.getUTCSeconds()) + "Z"; + var url = 'https://predict.cusf.co.uk/api/v1/'; + url += '?launch_latitude='+data.lat + '&launch_longitude='+data.lon; + url += '&launch_altitude='+data.alt + '&launch_datetime='+datetime; + url += '&ascent_rate='+ascent + '&burst_altitude=' + burst + '&descent_rate='+descent; + + $.getJSON(url, function( prediction ) { + draw_predict(prediction,data); + }); + }; + + draw_predict = function(prediction,data) { + var ascending = prediction.prediction[0].trajectory; + var highest = ascending[ascending.length-1]; + var highest_location = [highest.latitude,highest.longitude]; + + var descending = prediction.prediction[1].trajectory; + var landing = descending[descending.length-1]; + var landing_location = [landing.latitude,landing.longitude]; + + if (!marker_landing) { + marker_landing = L.marker(landing_location,{icon: icon_landing}).addTo(map) + .bindPopup(poptxt('landing',landing),{closeOnClick:false, autoPan:false}); + } else { + marker_landing.slideTo(landing_location, { + duration: 500, + keepAtCenter: (follow=='landing')?true:false + }) + .setPopupContent(poptxt('landing',landing)); + } + + dots_predict=[]; + + if (data.climb > 0) { + ascending.forEach(p => dots_predict.push([p.latitude,p.longitude])); + + if (!marker_burst) { + marker_burst = L.marker(highest_location,{icon:icon_burst}).addTo(map).bindPopup(poptxt('burst',highest),{closeOnClick:false, autoPan:false}); + } else { + marker_burst.slideTo(highest_location, { + duration: 500, + keepAtCenter: (follow=='burst')?true:false + }).setPopupContent(poptxt('burst',highest)); + } + } + + descending.forEach(p => dots_predict.push([p.latitude,p.longitude])); + line_predict.setLatLngs(dots_predict); + + if (data.climb > 0) { + predictor_time = 5 * 60; // ascending, every 5 min + } else if (data.climb < 0 && data.alt > 5000) { + predictor_time = 2 * 60; // descending, above 5km, every 2 min + } else { + predictor_time = 30; // descending, below 5km, every 30 sec + } + clearTimeout(predictor); + predictor = setTimeout(function() {get_predict(last_data);}, predictor_time*1000); + }; + + poptxt = function(t,i) { + var lat_input = (i.id)?i.lat:i.latitude; + var lon_input = (i.id)?i.lon:i.longitude; + + var lat = Math.round(lat_input * 1000000) / 1000000; + var lon = Math.round(lon_input * 1000000) / 1000000; + + var add = + '
Position: '+lat+', '+lon+'
'+ + 'Open: GMaps |Β OSM |Β Maps.me'; + + if (t == 'position') { return '
🎈 '+i.id+''+add+'
'; } + if (t == 'burst') { return '
πŸ’₯ Predicted Burst:
'+fd(i.datetime)+' in '+mr(i.altitude)+'m'+add+'
'; } + if (t == 'highest') { return '
πŸ’₯ Burst: '+mr(i.altitude)+'m'+add+'
';} + if (t == 'landing') { return '
🎯 Predicted Landing:
'+fd(i.datetime)+' at '+mr(i.altitude)+'m'+add+'
'; } + if (t == 'gps') { return '
Position: '+(i.lat/1000000)+','+(i.lon/1000000)+'
Altitude: '+mr(i.alt/1000)+'m
Speed: '+mr(i.speed/1000 * 1.852 * 10)/10+'km/h '+mr(i.dir/1000)+'Β°
Sat: '+i.sat+' Hdop:'+(i.hdop/10)+'
'; } + }; + + fd = function(date) { + var d = new Date(Date.parse(date)); + return az(d.getUTCHours()) +':'+ az(d.getUTCMinutes())+' UTC'; + }; + az = function(n) { return (n<10)?'0'+n:n; }; + mr = function(n) { return Math.round(n); }; + + storage = (typeof(Storage) !== "undefined")?true:false; + storage_write = function (data) { + if (storage) { + if (sessionStorage.sonde) { + storage_data = JSON.parse(sessionStorage.sonde); + } else { + storage_data = []; + } + if (JSON.stringify(data) != JSON.stringify(storage_data[storage_data.length - 1])) { + storage_data.push(data); + sessionStorage.sonde = JSON.stringify(storage_data); + } + } + }; + + storage_read = function() { + if (storage) { + if (sessionStorage.sonde) { + storage_data = JSON.parse(sessionStorage.sonde); + return storage_data; + } + } + return false; + }; + + storage_remove = function() { + sessionStorage.removeItem('sonde'); + }; + + session_storage = storage_read(); + if (session_storage) { + session_storage.forEach(function(d) { + dots.push([d.lat,d.lon,d.alt]); + session_storage_last = d; + }); + draw(session_storage_last); + } + + setInterval(get_data,1000); + +}); + +L.Control.Button = L.Control.extend({ + onAdd: function (map) { + var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control'); + options = this.options; + Object.keys(options).forEach(function(key) { + this.link = L.DomUtil.create('a', '', container); + this.link.text = options[key].text; + this.link.href = options[key].href; + this.link.id = options[key].id; + }); + + this.options.position = this.options[0].position; + return container; + } +}); + + +// https://github.com/makinacorpus/Leaflet.GeometryUtil/blob/master/src/leaflet.geometryutil.js#L682 +// modified to fit +function bearing(latlng1, latlng2) { + var rad = Math.PI / 180, + lat1 = latlng1[0] * rad, + lat2 = latlng2[0] * rad, + lon1 = latlng1[1] * rad, + lon2 = latlng2[1] * rad, + y = Math.sin(lon2 - lon1) * Math.cos(lat2), + x = Math.cos(lat1) * Math.sin(lat2) - + Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); + var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360; + bearing = bearing < 0 ? bearing-360 : bearing; + return Math.round(bearing); +} diff --git a/RX_FSK/data/style.css b/RX_FSK/data/style.css old mode 100644 new mode 100755 index 6fb7ea2..c0b7281 --- a/RX_FSK/data/style.css +++ b/RX_FSK/data/style.css @@ -128,3 +128,99 @@ p{ margin: 0; display: block; } + +#map { + height: 100%; +} + +.leaflet-popup-content table, .leaflet-popup-content table td { + border:0; + background-color: white; +} + +.leaflet-popup-content table td:nth-child(2),.leaflet-popup-content table td:nth-child(5) { + text-align: right; + padding-left: 3px; +} + +.leaflet-popup-content table td:nth-child(3),.leaflet-popup-content table td:nth-child(6) { + text-align: left; + padding-right: 10px; +} + +.leaflet-gps{animation:fading 1s infinite}@keyframes fading{0%{opacity:0.7}50%{opacity:1}100%{opacity:0.7}} + +.leaflet-gps::after { + content: 'πŸ”΅'; +} +.leaflet-gps { + margin-left: -7px !important; + margin-top: -9px !important; +} + +.leaflet-burst::after { + content: 'πŸ’₯'; +} +.leaflet-burst { + margin-left: -20px !important; + margin-top: -22px !important; + font-weight: bold; + font-size: 30px; +} + +.leaflet-landing::after { + content: 'Γ—'; +} + +.leaflet-landing { + margin-left: -13px !important; + margin-top: -30px !important; + font-weight: bold; + font-size: 40px; +} + + +.leaflet-header { + text-align: center; + width: 250px; + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.leaflet-footer { + display:none; + text-align: center; + width: 180px; + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.leaflet-center { + left:0; + right:0; + margin: 0 auto; + padding: 5px; + background: #fff; + background: rgba(255, 255, 255, 0.8); +} + +.leaflet-header #sonde_detail { + display:none; +} + +@media screen and (max-width: 600px) { + .leaflet-control-attribution { + -moz-transform: rotate(-90deg) translateX(100%); + -ms-transform: rotate(-90deg) translateX(100%); + -o-transform: rotate(-90deg) translateX(100%);; + -webkit-transform: rotate(-90deg) translateX(100%); + transform: rotate(-90deg) translateX(100%); + -webkit-transform-origin: 100% 100%; + -moz-transform-origin: 100% 100%; + -ms-transform-origin: 100% 100%; + -o-transform-origin: 100% 100%; + transform-origin: 100% 100%; + } +}