OTA close-WebUI hint, OTA diagnostic logging, WebUI radar improvements
The OTA confirmation dialog now warns to close the WebUI page first - its 3-second polling was interfering with the firmware download. performUpdate() now logs progress and, on failure, the actual HTTPUpdate error code/string over serial instead of just returning false, to make future OTA failures easier to diagnose. WebUI live radar: all four compass directions (N/E/S/W) instead of just N, clickable aircraft with a persistent info panel (callsign, altitude, speed, distance/bearing, heading, squawk), a client-side range selector (10/25/50/100 km, independent of the device setting), and twinkling background stars outside the radar circle redrawn at 150ms independent of the 3s aircraft-data poll. Note: the "notable" (orange ring) field in /radar.json remains hardcoded false - Config::NOTABLE_CALLSIGN_PREFIXES still doesn't exist anywhere in this project. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -246,7 +246,7 @@ static const char* const I18N_DE[] = {
|
||||
"Prüfung fehlgeschlagen. WLAN prüfen.",
|
||||
"Bereits aktuell: v",
|
||||
"Update verfügbar: v",
|
||||
"Ein neues Firmware-Update wird heruntergeladen und installiert. Bitte WLAN-Verbindung und Stromversorgung während des Vorgangs nicht unterbrechen. Nach der Installation wirst du zum Neustart aufgefordert.",
|
||||
"Ein neues Firmware-Update wird heruntergeladen und installiert. Bitte WLAN-Verbindung und Stromversorgung während des Vorgangs nicht unterbrechen. Nach der Installation wirst du zum Neustart aufgefordert. Falls die WebUI-Seite (Logbuch/Radar) in einem Browser geöffnet ist, bitte vorher schließen - sie fragt regelmäßig Daten vom Gerät ab und kann den Download sonst stören.",
|
||||
"Installiere Update ",
|
||||
"Update fehlgeschlagen",
|
||||
"Update installiert",
|
||||
|
||||
@@ -247,7 +247,7 @@ static const char* const I18N_EN[] = {
|
||||
"Check failed. Check your WiFi connection.",
|
||||
"Already up to date: v",
|
||||
"Update available: v",
|
||||
"A new firmware update will be downloaded and installed. Please don't interrupt the WiFi connection or power supply during this process. You'll be asked to restart once installation is complete.",
|
||||
"A new firmware update will be downloaded and installed. Please don't interrupt the WiFi connection or power supply during this process. You'll be asked to restart once installation is complete. If the WebUI page (logbook/radar) is open in a browser, please close it first - it regularly polls the device for data and can interfere with the download otherwise.",
|
||||
"Installing update ",
|
||||
"Update failed",
|
||||
"Update installed",
|
||||
|
||||
@@ -246,7 +246,7 @@ static const char* const I18N_ES[] = {
|
||||
"Error al comprobar. Revisa tu conexión WiFi.",
|
||||
"Ya actualizado: v",
|
||||
"Actualización disponible: v",
|
||||
"Se descargará e instalará una nueva actualización de firmware. No interrumpas la conexión WiFi ni la alimentación durante este proceso. Se te pedirá reiniciar una vez completada la instalación.",
|
||||
"Se descargará e instalará una nueva actualización de firmware. No interrumpas la conexión WiFi ni la alimentación durante este proceso. Se te pedirá reiniciar una vez completada la instalación. Si la página WebUI (registro/radar) está abierta en un navegador, ciérrala primero - consulta datos del dispositivo regularmente y podría interferir con la descarga.",
|
||||
"Instalando actualización ",
|
||||
"Error al actualizar",
|
||||
"Actualización instalada",
|
||||
|
||||
@@ -246,7 +246,7 @@ static const char* const I18N_FR[] = {
|
||||
"Échec de la vérification. Vérifiez votre connexion WiFi.",
|
||||
"Déjà à jour : v",
|
||||
"Mise à jour disponible : v",
|
||||
"Une nouvelle mise à jour du micrologiciel va être téléchargée et installée. Merci de ne pas interrompre la connexion WiFi ni l'alimentation pendant ce processus. Un redémarrage vous sera demandé une fois l'installation terminée.",
|
||||
"Une nouvelle mise à jour du micrologiciel va être téléchargée et installée. Merci de ne pas interrompre la connexion WiFi ni l'alimentation pendant ce processus. Un redémarrage vous sera demandé une fois l'installation terminée. Si la page WebUI (journal/radar) est ouverte dans un navigateur, merci de la fermer au préalable - elle interroge régulièrement l'appareil et pourrait sinon perturber le téléchargement.",
|
||||
"Installation de la mise à jour ",
|
||||
"Échec de la mise à jour",
|
||||
"Mise à jour installée",
|
||||
|
||||
@@ -246,7 +246,7 @@ static const char* const I18N_IT[] = {
|
||||
"Verifica fallita. Controlla la connessione WiFi.",
|
||||
"Già aggiornato: v",
|
||||
"Aggiornamento disponibile: v",
|
||||
"Un nuovo aggiornamento del firmware verrà scaricato e installato. Non interrompere la connessione WiFi o l'alimentazione durante questo processo. Al termine dell'installazione ti verrà chiesto di riavviare.",
|
||||
"Un nuovo aggiornamento del firmware verrà scaricato e installato. Non interrompere la connessione WiFi o l'alimentazione durante questo processo. Al termine dell'installazione ti verrà chiesto di riavviare. Se la pagina WebUI (registro/radar) è aperta in un browser, chiudila prima - interroga regolarmente il dispositivo e potrebbe interferire con il download.",
|
||||
"Installazione aggiornamento ",
|
||||
"Aggiornamento fallito",
|
||||
"Aggiornamento installato",
|
||||
|
||||
@@ -246,7 +246,7 @@ static const char* const I18N_TR[] = {
|
||||
"Kontrol başarısız. WiFi bağlantınızı kontrol edin.",
|
||||
"Zaten güncel: v",
|
||||
"Güncelleme mevcut: v",
|
||||
"Yeni bir donanım yazılımı güncellemesi indirilip yüklenecek. Lütfen bu işlem sırasında WiFi bağlantısını veya güç kaynağını kesmeyin. Kurulum tamamlandığında yeniden başlatmanız istenecek.",
|
||||
"Yeni bir donanım yazılımı güncellemesi indirilip yüklenecek. Lütfen bu işlem sırasında WiFi bağlantısını veya güç kaynağını kesmeyin. Kurulum tamamlandığında yeniden başlatmanız istenecek. WebUI sayfası (kayıt defteri/radar) bir tarayıcıda açıksa, lütfen önce kapatın - cihazdan düzenli olarak veri istediği için indirmeyi aksatabilir.",
|
||||
"Güncelleme yükleniyor ",
|
||||
"Güncelleme başarısız",
|
||||
"Güncelleme yüklendi",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ota_update.h"
|
||||
#include "config.h"
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <HTTPUpdate.h>
|
||||
@@ -68,6 +69,9 @@ CheckInfo checkForUpdate() {
|
||||
strncpy(info.latestVersion, (tag[0] == 'v' || tag[0] == 'V') ? tag + 1 : tag,
|
||||
sizeof(info.latestVersion) - 1);
|
||||
|
||||
// Der Release-Workflow (siehe CLAUDE.md im Repo) laedt das rohe
|
||||
// Build-Artefakt unveraendert als "firmware.bin" ins Release hoch -
|
||||
// keine Umbenennung mehr noetig.
|
||||
JsonArray assets = doc["assets"];
|
||||
for (JsonObject asset : assets) {
|
||||
const char* name = asset["name"] | "";
|
||||
@@ -86,6 +90,15 @@ CheckInfo checkForUpdate() {
|
||||
}
|
||||
|
||||
bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) {
|
||||
// Diagnose-Logging (nur ueber USB-Seriell sichtbar, kein Einfluss auf
|
||||
// die UI) - vorher wurde bei einem Fehlschlag nur ein simples "true/
|
||||
// false" nach aussen gegeben, ohne den eigentlichen Grund (Timeout,
|
||||
// TLS-Fehler, HTTP-Statuscode...) festzuhalten. Damit laesst sich ein
|
||||
// fehlgeschlagener OTA-Versuch am Seriell-Monitor nachvollziehen, statt
|
||||
// erneut raten zu muessen.
|
||||
Serial.printf("[OTA] Start: url=%s freeHeap=%u RSSI=%ddBm\n", url,
|
||||
(unsigned)ESP.getFreeHeap(), WiFi.RSSI());
|
||||
|
||||
WiFiClientSecure client;
|
||||
client.setInsecure();
|
||||
client.setTimeout(15000);
|
||||
@@ -108,9 +121,29 @@ bool performUpdate(const char* url, void (*onProgress)(uint8_t percent)) {
|
||||
if (onProgress && total > 0) {
|
||||
onProgress((uint8_t)((cur * 100) / total));
|
||||
}
|
||||
// Nur gelegentlich loggen (alle ~10%), sonst quillt der Seriell-
|
||||
// Monitor bei grossen Dateien mit hunderten Zeilen ueber.
|
||||
static int8_t lastLoggedPercent = -1;
|
||||
if (total > 0) {
|
||||
int8_t pct = (int8_t)((cur * 100) / total);
|
||||
if (pct != lastLoggedPercent && pct % 10 == 0) {
|
||||
lastLoggedPercent = pct;
|
||||
Serial.printf("[OTA] Fortschritt: %d%% (%d/%d Bytes) freeHeap=%u\n", pct, cur, total,
|
||||
(unsigned)ESP.getFreeHeap());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
t_httpUpdate_return result = httpUpdate.update(client, url);
|
||||
|
||||
if (result != HTTP_UPDATE_OK) {
|
||||
Serial.printf("[OTA] Fehlgeschlagen: result=%d error=%d (%s) freeHeap=%u\n", (int)result,
|
||||
httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str(),
|
||||
(unsigned)ESP.getFreeHeap());
|
||||
} else {
|
||||
Serial.println("[OTA] Erfolgreich heruntergeladen und geflasht.");
|
||||
}
|
||||
|
||||
return result == HTTP_UPDATE_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <ArduinoJson.h>
|
||||
#include <SD.h>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
namespace WebExportServer {
|
||||
|
||||
@@ -44,13 +45,15 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
// "Auffaellig" beschraenkt sich aktuell auf die Heavy-Kategorie (ADS-B
|
||||
// Emitter-Kategorie "A5") - der Teil ueber Militaer-/Regierungs-
|
||||
// Rufzeichen-Praefixe (Config::NOTABLE_CALLSIGN_PREFIXES) ist NICHT
|
||||
// umgesetzt, weil diese Konstante nirgends im Projekt existiert (auch
|
||||
// nicht in radar_screen.cpp) - das zugehoerige Feature wurde bisher nie
|
||||
// tatsaechlich spezifiziert/implementiert. Sobald es das gibt, hier den
|
||||
// Praefix-Abgleich ergaenzen, analog zu isEmergencySquawkWeb() oben.
|
||||
// "Auffaellig" (oranger Ring) beschraenkt sich aktuell auf gar nichts -
|
||||
// der Teil ueber Militaer-/Regierungs-Rufzeichen-Praefixe
|
||||
// (Config::NOTABLE_CALLSIGN_PREFIXES) ist NICHT umgesetzt, weil diese
|
||||
// Konstante nirgends im Projekt existiert (auch nicht in
|
||||
// radar_screen.cpp) - das zugehoerige Feature wurde bisher nie
|
||||
// tatsaechlich spezifiziert/implementiert. "notable" wird deshalb unten
|
||||
// in handleRadarJson() fest auf false gesetzt. Sobald es eine echte
|
||||
// Praefixliste gibt, hier eine isNotableCallsignWeb()-Funktion analog zu
|
||||
// isEmergencySquawkWeb() ergaenzen.
|
||||
bool isHeavyCategoryWeb(const char* category) {
|
||||
return category[0] == 'A' && category[1] == '5';
|
||||
}
|
||||
@@ -78,8 +81,12 @@ namespace {
|
||||
html += ".addbtn:hover{background:#39ff14;color:#0a0f0d;}";
|
||||
html += "input[type=text]{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:5px 8px;font-family:inherit;}";
|
||||
html += "nav{margin-bottom:10px;}nav a{color:#39ff14;margin-right:16px;}";
|
||||
html += "#radarCanvas{width:100%;max-width:400px;height:auto;background:#05100a;border:1px solid #1f3a2b;border-radius:8px;display:block;}";
|
||||
html += "#radarCanvas{width:100%;max-width:400px;height:auto;background:#05100a;border:1px solid #1f3a2b;border-radius:8px;display:block;cursor:pointer;}";
|
||||
html += "#radarStatus{font-size:12px;color:#7a9a86;margin-top:4px;}";
|
||||
html += "#radarControls{font-size:12px;margin-bottom:8px;}";
|
||||
html += "#radarControls select{background:#0a0f0d;color:#39ff14;border:1px solid #39ff14;border-radius:4px;padding:2px 6px;font-family:inherit;}";
|
||||
html += "#acInfo{display:none;max-width:400px;margin-top:8px;padding:8px 10px;border:1px solid #39ff14;border-radius:6px;font-size:13px;line-height:1.7;}";
|
||||
html += "#acInfo a{color:#ff3b3b;text-decoration:none;border:1px solid #ff3b3b;border-radius:4px;padding:2px 8px;display:inline-block;margin-top:4px;}";
|
||||
html += "</style></head><body>";
|
||||
html += "<h1>" + title + "</h1>";
|
||||
return html;
|
||||
@@ -91,24 +98,73 @@ namespace {
|
||||
// fetch()-Polling statt WebSocket/SSE gehalten - deutlich weniger Code
|
||||
// und Speicherbedarf auf dem ESP32, und fuer eine gelegentlich vom Handy
|
||||
// aus aufgerufene Seite voellig ausreichend.
|
||||
//
|
||||
// Der Reichweiten-Waehler (<select>) ist rein clientseitig/pro Seiten-
|
||||
// aufruf - er aendert NICHT die Geraete-Einstellung (SettingsStore::
|
||||
// rangeIndex()), sondern wird als "range_km"-Query-Parameter an
|
||||
// /radar.json mitgeschickt (siehe handleRadarJson()) und erlaubt so ein
|
||||
// unabhaengiges Herein-/Herauszoomen auf dem Handy, ohne das Geraete-
|
||||
// Display zu beeinflussen. Default ist die aktuelle Geraete-Reichweite.
|
||||
void appendRadarSection(String& html) {
|
||||
float deviceRangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
|
||||
|
||||
html += "<h2>Live Radar</h2>";
|
||||
html += "<div id=\"radarControls\">Range: <select id=\"radarRange\">";
|
||||
for (uint8_t i = 0; i < Config::RANGE_STEP_COUNT; i++) {
|
||||
bool isDefault = fabsf(Config::RANGE_STEPS_KM[i] - deviceRangeKm) < 0.5f;
|
||||
html += "<option value=\"" + String(Config::RANGE_STEPS_KM[i], 0) + "\"";
|
||||
if (isDefault) html += " selected";
|
||||
html += ">" + String(Config::RANGE_STEPS_KM[i], 0) + " km</option>";
|
||||
}
|
||||
html += "</select></div>";
|
||||
html += "<canvas id=\"radarCanvas\" width=\"360\" height=\"360\"></canvas>";
|
||||
html += "<p id=\"radarStatus\">Loading...</p>";
|
||||
html += "<div id=\"acInfo\"></div>";
|
||||
html += "<script>(function(){";
|
||||
html += "var canvas=document.getElementById('radarCanvas');";
|
||||
html += "var ctx=canvas.getContext('2d');";
|
||||
html += "var status=document.getElementById('radarStatus');";
|
||||
html += "var infoBox=document.getElementById('acInfo');";
|
||||
html += "var rangeSel=document.getElementById('radarRange');";
|
||||
html += "var W=canvas.width,H=canvas.height,cx=W/2,cy=H/2,R=Math.min(W,H)/2-24;";
|
||||
html += "var lastData={range_km:" + String(deviceRangeKm, 0) + ",aircraft:[]};";
|
||||
html += "var markers=[];";
|
||||
html += "var selectedHex=null;";
|
||||
|
||||
// Hintergrund-Sterne AUSSERHALB des Radarkreises - gleiches Prinzip
|
||||
// wie updateBgStars()/initBgStarsIfNeeded() in radar_screen.cpp
|
||||
// (Rejection-Sampling, damit kein Stern innerhalb des Kreises
|
||||
// landet), hier per Canvas/JS statt TFT_eSPI nachgebaut.
|
||||
html += "var stars=[];";
|
||||
html += "(function(){var minDistSq=(R+6)*(R+6);for(var i=0;i<24;i++){var x,y,tries=0;";
|
||||
html += "do{x=4+Math.random()*(W-8);y=4+Math.random()*(H-8);tries++;}";
|
||||
html += "while(((x-cx)*(x-cx)+(y-cy)*(y-cy))<minDistSq&&tries<25);";
|
||||
html += "stars.push({x:x,y:y,phase:Math.random()*255,speed:1+Math.random()*2});}})();";
|
||||
html += "function drawStars(){for(var i=0;i<stars.length;i++){var s=stars[i];";
|
||||
html += "s.phase=(s.phase+s.speed)%256;";
|
||||
html += "var bright=Math.round(s.phase<128?s.phase*2:(255-s.phase)*2);";
|
||||
html += "ctx.fillStyle='rgb(0,'+bright+',0)';ctx.fillRect(s.x,s.y,1,1);}}";
|
||||
|
||||
html += "function altColor(ft){if(ft<3000)return '#ff4d4d';if(ft<10000)return '#ffb84d';if(ft<25000)return '#ffe14d';if(ft<35000)return '#39ff14';return '#4dd2ff';}";
|
||||
|
||||
html += "function draw(data){";
|
||||
html += "lastData=data;";
|
||||
html += "ctx.clearRect(0,0,W,H);";
|
||||
html += "drawStars();";
|
||||
html += "ctx.strokeStyle='#1f3a2b';ctx.fillStyle='#39ff14';ctx.font='10px monospace';ctx.textAlign='left';";
|
||||
html += "for(var ring=1;ring<=3;ring++){var r=R*ring/3;ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.stroke();";
|
||||
html += "ctx.fillText(Math.round(data.range_km*ring/3)+' km',cx+4,cy-r+10);}";
|
||||
html += "ctx.strokeStyle='#12261a';ctx.beginPath();ctx.moveTo(cx-R,cy);ctx.lineTo(cx+R,cy);ctx.moveTo(cx,cy-R);ctx.lineTo(cx,cy+R);ctx.stroke();";
|
||||
// Alle vier Himmelsrichtungen (N/E/S/W), genau wie
|
||||
// drawStaticBackground() in radar_screen.cpp - vorher stand hier nur
|
||||
// "N", was auf Nachfrage ergaenzt wurde.
|
||||
html += "ctx.fillStyle='#39ff14';ctx.textAlign='center';ctx.fillText('N',cx,cy-R-8);";
|
||||
html += "ctx.fillText('S',cx,cy+R+16);";
|
||||
html += "ctx.textAlign='left';ctx.fillText('E',cx+R+4,cy+3);";
|
||||
html += "ctx.textAlign='right';ctx.fillText('W',cx-R-4,cy+3);";
|
||||
html += "ctx.textAlign='center';";
|
||||
html += "ctx.fillStyle='#ffffff';ctx.beginPath();ctx.arc(cx,cy,3,0,Math.PI*2);ctx.fill();";
|
||||
html += "markers=[];";
|
||||
html += "(data.aircraft||[]).forEach(function(a){";
|
||||
html += "var theta=a.bearing_deg*Math.PI/180;";
|
||||
html += "var r=Math.min(a.dist_km/data.range_km,1)*R;";
|
||||
@@ -124,12 +180,65 @@ namespace {
|
||||
html += "if(a.emergency){ctx.strokeStyle='#ff3b3b';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
|
||||
html += "else if(a.watched){ctx.strokeStyle='#00e5ff';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
|
||||
html += "else if(a.notable){ctx.strokeStyle='#ff9f1a';ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.stroke();}";
|
||||
// Ausgewaehltes Flugzeug (per Klick/Tap, siehe unten) bekommt einen
|
||||
// weissen Auswahlring, gleiches Prinzip wie isSelected auf dem
|
||||
// Geraete-Display (radar_screen.cpp render()).
|
||||
html += "if(a.hex===selectedHex){ctx.strokeStyle='#ffffff';ctx.beginPath();ctx.arc(x,y,11,0,Math.PI*2);ctx.stroke();}";
|
||||
html += "ctx.fillStyle=color;ctx.textAlign='center';ctx.fillText(a.callsign,x,y-8);";
|
||||
html += "markers.push({x:x,y:y,a:a});";
|
||||
html += "});";
|
||||
html += "status.textContent=(data.aircraft||[]).length+' aircraft \\u00b7 range '+data.range_km+' km';";
|
||||
// Falls das ausgewaehlte Flugzeug in dieser Aktualisierung nicht
|
||||
// mehr vorkommt (z.B. aus der Reichweite geflogen), Infobox wieder
|
||||
// schliessen statt veraltete Daten stehen zu lassen.
|
||||
html += "if(selectedHex){var found=markers.filter(function(m){return m.a.hex===selectedHex;})[0];";
|
||||
html += "if(found){showInfo(found.a);}else{selectedHex=null;hideInfo();}}";
|
||||
html += "}";
|
||||
html += "function poll(){fetch('/radar.json').then(function(r){return r.json();}).then(draw).catch(function(){status.textContent='Connection lost - retrying...';});}";
|
||||
html += "poll();setInterval(poll,3000);";
|
||||
|
||||
// Info-Panel fuer ein angetipptes Flugzeug - bewusst eine eigene,
|
||||
// stehenbleibende Box (kein Tooltip/Popup, das beim naechsten
|
||||
// Neuzeichnen einfach verschwindet), mit explizitem Schliessen-Link,
|
||||
// gleiches Grundprinzip wie infoScreen() am Geraet: der Nutzer soll
|
||||
// aktiv entscheiden, wann die Info wieder verschwindet.
|
||||
html += "function fmtNum(n,d){return (typeof n==='number')?n.toFixed(d):'?';}";
|
||||
html += "function showInfo(a){";
|
||||
html += "var lines=[];";
|
||||
html += "lines.push('<b>'+(a.callsign||a.hex)+'</b> ('+a.hex+')');";
|
||||
html += "lines.push('Altitude: '+Math.round(a.alt_ft)+' ft');";
|
||||
html += "if(a.speed_kt){lines.push('Speed: '+Math.round(a.speed_kt)+' kt');}";
|
||||
html += "lines.push('Distance: '+fmtNum(a.dist_km,1)+' km, bearing '+Math.round(a.bearing_deg)+'\\u00b0');";
|
||||
html += "lines.push('Heading: '+Math.round(a.track_deg)+'\\u00b0');";
|
||||
html += "if(a.squawk){lines.push('Squawk: '+a.squawk);}";
|
||||
html += "infoBox.innerHTML=lines.join('<br>')+'<br><a href=\"#\" id=\"acInfoClose\">Close</a>';";
|
||||
html += "infoBox.style.display='block';";
|
||||
html += "document.getElementById('acInfoClose').onclick=function(e){e.preventDefault();selectedHex=null;hideInfo();draw(lastData);};";
|
||||
html += "}";
|
||||
html += "function hideInfo(){infoBox.style.display='none';}";
|
||||
|
||||
html += "canvas.addEventListener('click',function(ev){";
|
||||
html += "var rect=canvas.getBoundingClientRect();";
|
||||
html += "var scaleX=canvas.width/rect.width,scaleY=canvas.height/rect.height;";
|
||||
html += "var px=(ev.clientX-rect.left)*scaleX,py=(ev.clientY-rect.top)*scaleY;";
|
||||
html += "var best=null,bestD=14*14;";
|
||||
html += "markers.forEach(function(m){var dx=m.x-px,dy=m.y-py,d=dx*dx+dy*dy;if(d<bestD){bestD=d;best=m;}});";
|
||||
html += "if(best){selectedHex=best.a.hex;showInfo(best.a);}else{selectedHex=null;hideInfo();}";
|
||||
html += "draw(lastData);";
|
||||
html += "});";
|
||||
|
||||
html += "if(rangeSel){rangeSel.addEventListener('change',poll);}";
|
||||
html += "function poll(){";
|
||||
html += "var url='/radar.json';";
|
||||
html += "if(rangeSel&&rangeSel.value){url+='?range_km='+rangeSel.value;}";
|
||||
html += "fetch(url).then(function(r){return r.json();}).then(draw).catch(function(){status.textContent='Connection lost - retrying...';});";
|
||||
html += "}";
|
||||
html += "poll();";
|
||||
html += "setInterval(poll,3000);";
|
||||
// Schnellerer, rein lokaler Redraw-Takt (alle 150ms, ohne Netzwerk-
|
||||
// Anfrage) nur fuer das Sternenfunkeln + die Auswahlmarkierung -
|
||||
// gleiches Grundprinzip wie tick() vs. render() auf dem
|
||||
// Geraete-Display: Flugzeugpositionen aktualisieren sich weiterhin
|
||||
// nur alle 3s per poll(), die Sterne twinkeln aber fluessig dazwischen.
|
||||
html += "setInterval(function(){draw(lastData);},150);";
|
||||
html += "})();</script>";
|
||||
}
|
||||
|
||||
@@ -348,6 +457,20 @@ namespace {
|
||||
// genau dieser Prioritaet.
|
||||
void handleRadarJson() {
|
||||
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
|
||||
// Erlaubt der Web-Ansicht ein eigenes, unabhaengiges Zoomen (siehe
|
||||
// Reichweiten-Waehler in appendRadarSection()), OHNE die Geraete-
|
||||
// Einstellung zu veraendern. Nur einen der bekannten
|
||||
// Config::RANGE_STEPS_KM-Werte akzeptieren - ein fehlender oder
|
||||
// nicht erkannter Parameter faellt auf die Geraete-Reichweite zurueck.
|
||||
if (server.hasArg("range_km")) {
|
||||
float requested = server.arg("range_km").toFloat();
|
||||
for (uint8_t i = 0; i < Config::RANGE_STEP_COUNT; i++) {
|
||||
if (fabsf(Config::RANGE_STEPS_KM[i] - requested) < 0.5f) {
|
||||
rangeKm = Config::RANGE_STEPS_KM[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool hideGround = SettingsStore::hideGroundVehicles();
|
||||
bool emergencyOn = SettingsStore::emergencyAlertEnabled();
|
||||
bool watchOn = SettingsStore::watchlistAlertEnabled();
|
||||
@@ -373,10 +496,11 @@ namespace {
|
||||
bool isWatched = watchOn && AircraftWatchlist::isWatched(a.callsign);
|
||||
// "notable" (oranger Ring) ist fuer auffaellige Rufzeichen
|
||||
// (Militaer/Regierung) reserviert - Heavy-Flugzeuge bekommen
|
||||
// stattdessen die eigene Markerform (siehe "heavy" unten). Es
|
||||
// stattdessen die eigene Markerform (siehe "heavy" oben). Es
|
||||
// gibt aktuell aber keine Militaer-/Regierungs-Praefixliste im
|
||||
// Projekt (auch nicht am Geraete-Display, siehe radar_screen.cpp)
|
||||
// - "notable" bleibt daher bis auf Weiteres immer false.
|
||||
// Projekt (auch nicht am Geraete-Display, siehe
|
||||
// radar_screen.cpp) - "notable" bleibt daher bis auf Weiteres
|
||||
// immer false.
|
||||
bool isNotable = false;
|
||||
|
||||
JsonObject o = arr.add<JsonObject>();
|
||||
@@ -392,6 +516,12 @@ namespace {
|
||||
o["emergency"] = isEmergency;
|
||||
o["watched"] = isWatched;
|
||||
o["notable"] = isNotable;
|
||||
// Zusaetzliche Felder nur fuer das Info-Panel bei Klick/Tap auf
|
||||
// ein Flugzeug (siehe showInfo() in appendRadarSection()) - beide
|
||||
// stehen bereits verlustfrei im Aircraft-Snapshot, kein
|
||||
// zusaetzlicher Netzwerk-/SD-Zugriff noetig.
|
||||
o["speed_kt"] = a.groundSpeedKt;
|
||||
o["squawk"] = a.squawk;
|
||||
}
|
||||
AircraftTable::unlock();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user