v2.7.5: Logbook Web UI menu entry, per-day CSV delete, remove screenshot feature

Adds a "Logbook / WebUI" entry directly in Menu > System, showing the
device's IP and usage instructions (previously only reachable via a
hidden "?" button in Logbook Files). The web export page now supports
downloading and deleting individual daily logbook CSV files, in
addition to the existing merged CSV download.

Fixes a task watchdog reset that could reboot the device mid-download
when exporting the logbook via the built-in web server: the CSV send
loop never yielded the CPU, starving the idle task on larger logbooks.
The export loop now yields periodically (delay(1) every 10 lines and
after each file).

Removes the screenshot feature (camera button, on-device BMP storage,
web-based screenshot browser). Screenshots always came out solid
black - diagnosis showed the SPI readback from the display returns all
zeros regardless of SPI_READ_FREQUENCY, indicating TFT_MISO isn't
wired to the display on this CYD board revision. This is a hardware
limitation, not fixable in software, so the feature has been removed
rather than shipped broken.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eiswolf-BG
2026-08-10 16:16:14 +02:00
parent cf9d096527
commit bb0f1d9e46
17 changed files with 300 additions and 170 deletions

View File

@@ -4,6 +4,7 @@
#include "sd_mutex.h"
#include <WebServer.h>
#include <SD.h>
#include <cstring>
namespace WebExportServer {
@@ -12,35 +13,60 @@ namespace {
WebServer server(80);
void handleRoot() {
FlightLogbook::DayEntry days[MAX_DAYS_QUERIED];
uint8_t count = FlightLogbook::listDays(days, MAX_DAYS_QUERIED);
// Nur reine Dateinamen/Labels aus Formularfeldern akzeptieren - kein
// "/" und kein ".." - damit ueber die WebUI kein Ausbruch aus dem
// jeweiligen SD-Verzeichnis moeglich ist (Pfad-Traversal).
bool isSafeName(const String& name) {
if (name.length() == 0 || name.length() > 40) return false;
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) return false;
if (name.indexOf("..") >= 0) return false;
return true;
}
String htmlHeader(const String& title) {
String html;
html += "<!DOCTYPE html><html><head><meta charset=\"UTF-8\">";
html += "<title>Eiswolfs Flightradar - Logbook Export</title>";
html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
html += "<title>" + title + "</title>";
html += "<style>";
html += "body{background-color:#0a0f0d;color:#39ff14;font-family:'Courier New',Courier,monospace;padding:20px;}";
html += "h1{font-size:20px;}";
html += "table{border-collapse:collapse;margin-top:10px;}";
html += "h1{font-size:20px;}h2{font-size:16px;margin-top:24px;border-top:1px solid #1f3a2b;padding-top:12px;}";
html += "table{border-collapse:collapse;margin-top:10px;width:100%;}";
html += "td,th{padding:4px 12px;text-align:left;border-bottom:1px solid #1f3a2b;}";
html += "a{color:#39ff14;}";
html += "form{display:inline;}";
html += "button{background:#0a0f0d;color:#ff3b3b;border:1px solid #ff3b3b;border-radius:4px;padding:3px 10px;font-family:inherit;cursor:pointer;}";
html += "button:hover{background:#ff3b3b;color:#0a0f0d;}";
html += ".dl{color:#39ff14;text-decoration:none;border:1px solid #39ff14;border-radius:4px;padding:3px 10px;margin-right:6px;display:inline-block;}";
html += "</style></head><body>";
html += "<h1>Eiswolfs Flightradar - Logbook Export</h1>";
html += "<h1>" + title + "</h1>";
return html;
}
if (count == 0) {
void handleRoot() {
FlightLogbook::DayEntry days[MAX_DAYS_QUERIED];
uint8_t dayCount = FlightLogbook::listDays(days, MAX_DAYS_QUERIED);
String html = htmlHeader("Eiswolfs Flightradar - Logbook Export");
html += "<h2>Flight Logbook</h2>";
if (dayCount == 0) {
html += "<p>No logbook entries yet.</p>";
} else {
html += "<table><tr><th>Date</th><th>Aircraft</th></tr>";
for (uint8_t i = 0; i < count; i++) {
html += "<tr><td>" + String(days[i].date) + "</td><td>" + String(days[i].count) + "</td></tr>";
html += "<p><a class=\"dl\" href=\"/export.csv\">Download merged CSV (all days)</a></p>";
html += "<table><tr><th>Date</th><th>Aircraft</th><th></th><th></th></tr>";
for (uint8_t i = 0; i < dayCount; i++) {
String date = String(days[i].date);
html += "<tr><td>" + date + "</td><td>" + String(days[i].count) + "</td>";
html += "<td><a class=\"dl\" href=\"/csv?date=" + date + "\">Download</a></td>";
html += "<td><form method=\"POST\" action=\"/logbook/delete\">";
html += "<input type=\"hidden\" name=\"date\" value=\"" + date + "\">";
html += "<button type=\"submit\">Delete</button></form></td></tr>";
}
html += "</table>";
html += "<p><a href=\"/export.csv\">Download merged CSV (all days)</a></p>";
}
html += "</body></html>";
server.send(200, "text/html", html);
}
@@ -53,6 +79,7 @@ namespace {
server.sendContent("date,timestamp,hex,callsign,reg,type,distance_km,altitude_ft\n");
SdMutex::Guard guard;
uint16_t linesSent = 0;
for (uint8_t i = 0; i < count; i++) {
char path[48];
snprintf(path, sizeof(path), "%s/%s.csv", Config::SD_LOG_DIR, days[i].date);
@@ -66,11 +93,58 @@ namespace {
line.trim();
if (line.length() == 0) continue;
server.sendContent(String(days[i].date) + "," + line + "\n");
// Wichtig: Ohne regelmaessiges Abgeben der CPU haengt dieser
// Task (Core 0, Prioritaet 1) die Idle-Task aus, die den
// Task-Watchdog fuettert. Bei groesseren Logbuechern fuehrt
// das nach ca. 5s ohne Yield zu einem Watchdog-Reset -
// genau der Reboot mitten im CSV-Download, den der Nutzer
// beobachtet hat. delay(1) erzwingt einen Kontextwechsel.
if (++linesSent % 10 == 0) {
delay(1);
}
}
f.close();
delay(1);
}
}
void handleCsvDownload() {
if (!server.hasArg("date")) {
server.send(400, "text/plain", "Bad request");
return;
}
String date = server.arg("date");
if (!isSafeName(date)) {
server.send(400, "text/plain", "Bad request");
return;
}
char path[64];
snprintf(path, sizeof(path), "%s/%s.csv", Config::SD_LOG_DIR, date.c_str());
SdMutex::Guard guard;
File f = SD.open(path, FILE_READ);
if (!f) {
server.send(404, "text/plain", "Not found");
return;
}
server.sendHeader("Content-Disposition", "attachment; filename=\"" + date + ".csv\"");
server.streamFile(f, "text/csv");
f.close();
}
void handleLogbookDelete() {
if (server.hasArg("date")) {
String date = server.arg("date");
if (isSafeName(date)) {
FlightLogbook::deleteFile(date.c_str());
}
}
server.sendHeader("Location", "/");
server.send(303);
}
void handleNotFound() {
server.send(404, "text/plain", "Not found");
}
@@ -79,6 +153,8 @@ namespace {
void begin() {
server.on("/", handleRoot);
server.on("/export.csv", handleExportCsv);
server.on("/csv", HTTP_GET, handleCsvDownload);
server.on("/logbook/delete", HTTP_POST, handleLogbookDelete);
server.onNotFound(handleNotFound);
server.begin();
}