repeater-mqtt-eastmesh-v1.3.8

This commit is contained in:
Jared Dohrman
2026-04-21 18:07:04 +10:00
rodzic 616d1c8213
commit 0ad50e5bd0
14 zmienionych plików z 748 dodań i 79 usunięć
+3
Wyświetl plik
@@ -37,6 +37,9 @@ const char* cardTypeName(uint8_t type) {
#if defined(ESP32)
SPIClass* getBoardSharedArchiveSPI() __attribute__((weak));
SPIClass* getBoardSharedArchiveSPI() {
return nullptr;
}
#endif
ArchiveStorage::ArchiveStorage()
+264 -44
Wyświetl plik
@@ -26,7 +26,7 @@ namespace {
constexpr uint32_t kSummaryFlushIntervalMs = 5UL * 60UL * 1000UL;
constexpr uint32_t kEventFlushIntervalMs = 60UL * 1000UL;
constexpr size_t kMaxSeriesPoints = 64;
constexpr size_t kSummaryRestoreWindowBytes = 16384;
constexpr size_t kSummaryRestoreWindowBytes = 32768;
constexpr size_t kEventsRestoreWindowBytes = 4096;
constexpr uint32_t kBootAutoCapturePsramMinBytes = 2000000UL;
constexpr size_t kLiveOnlySampleCapacity = 24;
@@ -187,28 +187,90 @@ File openArchiveWriteWithRecovery(ArchiveStorage* archive, const char* filename)
return fs != nullptr ? openArchiveWrite(fs, filename) : File();
}
int buildPointValue(const HistorySample& sample, const HistorySample* previous, const char* series) {
bool buildPointValue(const HistorySample& sample, const HistorySample* previous, const char* series, int& value) {
if (strcmp(series, "battery") == 0) {
return static_cast<int>(sample.battery_mv);
value = static_cast<int>(sample.battery_mv);
return true;
}
if (strcmp(series, "memory") == 0) {
return static_cast<int>(sample.heap_free);
value = static_cast<int>(sample.heap_free);
return true;
}
if (strcmp(series, "signal") == 0) {
return static_cast<int>(sample.last_rssi_x4);
value = static_cast<int>(sample.last_rssi_x4);
return true;
}
if (strcmp(series, "noise_floor") == 0) {
return static_cast<int>(sample.noise_floor * 4);
value = static_cast<int>(sample.noise_floor * 4);
return true;
}
if (strcmp(series, "packets") == 0) {
if (previous == nullptr) {
return 0;
value = 0;
return true;
}
const uint32_t curr_total = sample.packets_sent + sample.packets_recv;
const uint32_t prev_total = previous->packets_sent + previous->packets_recv;
return static_cast<int>(curr_total >= prev_total ? (curr_total - prev_total) : 0);
value = static_cast<int>(curr_total >= prev_total ? (curr_total - prev_total) : 0);
return true;
}
return 0;
if (strcmp(series, "voltage") == 0) {
if ((sample.sensor_flags & HISTORY_SENSOR_SUPPLY_VOLTAGE) == 0) {
return false;
}
value = static_cast<int>(sample.supply_voltage_centi_v);
return true;
}
if (strcmp(series, "sensor_temp") == 0) {
if ((sample.sensor_flags & HISTORY_SENSOR_TEMP) == 0) {
return false;
}
value = static_cast<int>(sample.sensor_temp_deci_c);
return true;
}
if (strcmp(series, "humidity") == 0) {
if ((sample.sensor_flags & HISTORY_SENSOR_HUMIDITY) == 0) {
return false;
}
value = static_cast<int>(sample.humidity_deci_pct);
return true;
}
if (strcmp(series, "pressure") == 0) {
if ((sample.sensor_flags & HISTORY_SENSOR_PRESSURE) == 0) {
return false;
}
value = static_cast<int>(sample.pressure_deci_hpa);
return true;
}
if (strcmp(series, "pressure_altitude") == 0) {
if ((sample.sensor_flags & HISTORY_SENSOR_PRESSURE_ALTITUDE) == 0) {
return false;
}
value = static_cast<int>(sample.pressure_altitude_m);
return true;
}
if (strcmp(series, "mcu_temp") == 0) {
if ((sample.sensor_flags & HISTORY_SENSOR_MCU_TEMP) == 0) {
return false;
}
value = static_cast<int>(sample.mcu_temp_deci_c);
return true;
}
if (strcmp(series, "gps_altitude") == 0) {
if ((sample.sensor_flags & HISTORY_SENSOR_GPS_ALTITUDE) == 0) {
return false;
}
value = static_cast<int>(sample.gps_altitude_m);
return true;
}
if (strcmp(series, "gps_satellites") == 0) {
if ((sample.sensor_flags & HISTORY_SENSOR_GPS_SATELLITES) == 0) {
return false;
}
value = static_cast<int>(sample.gps_satellites);
return true;
}
return false;
}
const char* seriesTitle(const char* series) {
@@ -227,6 +289,30 @@ const char* seriesTitle(const char* series) {
if (strcmp(series, "noise_floor") == 0) {
return "Noise Floor";
}
if (strcmp(series, "voltage") == 0) {
return "Voltage";
}
if (strcmp(series, "sensor_temp") == 0) {
return "Sensor Temp";
}
if (strcmp(series, "humidity") == 0) {
return "Humidity";
}
if (strcmp(series, "pressure") == 0) {
return "Barometer";
}
if (strcmp(series, "pressure_altitude") == 0) {
return "Pressure Altitude";
}
if (strcmp(series, "mcu_temp") == 0) {
return "MCU Temp";
}
if (strcmp(series, "gps_altitude") == 0) {
return "GPS Altitude";
}
if (strcmp(series, "gps_satellites") == 0) {
return "Satellites";
}
return "";
}
@@ -246,6 +332,24 @@ const char* seriesUnit(const char* series) {
if (strcmp(series, "noise_floor") == 0) {
return "noise_floor_x4";
}
if (strcmp(series, "voltage") == 0) {
return "centi_v";
}
if (strcmp(series, "sensor_temp") == 0 || strcmp(series, "mcu_temp") == 0) {
return "deci_c";
}
if (strcmp(series, "humidity") == 0) {
return "deci_pct";
}
if (strcmp(series, "pressure") == 0) {
return "deci_hpa";
}
if (strcmp(series, "pressure_altitude") == 0 || strcmp(series, "gps_altitude") == 0) {
return "m";
}
if (strcmp(series, "gps_satellites") == 0) {
return "count";
}
return "";
}
@@ -486,8 +590,85 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con
unsigned direct_dups = 0;
unsigned flood_dups = 0;
unsigned flags = 0;
unsigned supply_voltage_centi_v = 0;
int sensor_temp_deci_c = 0;
int mcu_temp_deci_c = 0;
unsigned humidity_deci_pct = 0;
unsigned pressure_deci_hpa = 0;
int pressure_altitude_m = 0;
int gps_altitude_m = 0;
long gps_lat_e6 = 0;
long gps_lon_e6 = 0;
unsigned sensor_flags = 0;
unsigned gps_satellites = 0;
int parsed = sscanf(line,
"%lu,%lu,%u,%u,%d,%d,%d,%lu,%lu,%lu,%lu,%u,%u,%u,%u,%u,%u,%u,%d,%d,%u,%u,%d,%d,%ld,%ld,%u,%u",
&epoch_secs,
&uptime_secs,
&battery_mv,
&queue_len,
&last_rssi_x4,
&last_snr_x4,
&noise_floor,
&packets_sent,
&packets_recv,
&heap_free,
&psram_free,
&error_flags,
&recv_errors,
&neighbour_count,
&direct_dups,
&flood_dups,
&flags,
&supply_voltage_centi_v,
&sensor_temp_deci_c,
&mcu_temp_deci_c,
&humidity_deci_pct,
&pressure_deci_hpa,
&pressure_altitude_m,
&gps_altitude_m,
&gps_lat_e6,
&gps_lon_e6,
&sensor_flags,
&gps_satellites);
if (parsed == 28) {
memset(&sample, 0, sizeof(sample));
sample.epoch_secs = static_cast<uint32_t>(epoch_secs);
sample.uptime_secs = static_cast<uint32_t>(uptime_secs);
sample.packets_sent = static_cast<uint32_t>(packets_sent);
sample.packets_recv = static_cast<uint32_t>(packets_recv);
sample.heap_free = static_cast<uint32_t>(heap_free);
sample.heap_min = static_cast<uint32_t>(heap_free);
sample.psram_free = static_cast<uint32_t>(psram_free);
sample.psram_min = static_cast<uint32_t>(psram_free);
sample.battery_mv = static_cast<uint16_t>(battery_mv);
sample.queue_len = static_cast<uint16_t>(queue_len);
sample.error_flags = static_cast<uint16_t>(error_flags);
sample.recv_errors = static_cast<uint16_t>(recv_errors);
sample.neighbour_count = static_cast<uint16_t>(neighbour_count);
sample.direct_dups = static_cast<uint16_t>(direct_dups);
sample.flood_dups = static_cast<uint16_t>(flood_dups);
sample.last_rssi_x4 = static_cast<int16_t>(last_rssi_x4);
sample.last_snr_x4 = static_cast<int16_t>(last_snr_x4);
sample.noise_floor = static_cast<int16_t>(noise_floor);
sample.supply_voltage_centi_v = static_cast<uint16_t>(supply_voltage_centi_v);
sample.sensor_temp_deci_c = static_cast<int16_t>(sensor_temp_deci_c);
sample.mcu_temp_deci_c = static_cast<int16_t>(mcu_temp_deci_c);
sample.humidity_deci_pct = static_cast<uint16_t>(humidity_deci_pct);
sample.pressure_deci_hpa = static_cast<uint16_t>(pressure_deci_hpa);
sample.pressure_altitude_m = static_cast<int16_t>(pressure_altitude_m);
sample.gps_altitude_m = static_cast<int16_t>(gps_altitude_m);
sample.gps_lat_e6 = static_cast<int32_t>(gps_lat_e6);
sample.gps_lon_e6 = static_cast<int32_t>(gps_lon_e6);
sample.sensor_flags = static_cast<uint16_t>(sensor_flags);
sample.gps_satellites = static_cast<uint8_t>(gps_satellites);
sample.flags = static_cast<uint8_t>(flags);
sample.battery_pct = -1;
return true;
}
parsed = sscanf(line,
"%lu,%lu,%u,%u,%d,%d,%d,%lu,%lu,%lu,%lu,%u,%u,%u,%u,%u,%u",
&epoch_secs,
&uptime_secs,
@@ -589,7 +770,7 @@ bool StatsHistory::restoreSummaryLog() {
return false;
}
char line[192];
char line[384];
size_t line_len = 0;
bool skip_partial = (start > 0);
while (file.available()) {
@@ -819,9 +1000,9 @@ void StatsHistory::flushSummaryLog() {
return;
}
char line[256];
char line[384];
snprintf(line, sizeof(line),
"%lu,%lu,%u,%u,%d,%d,%d,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u\n",
"%lu,%lu,%u,%u,%d,%d,%d,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%d,%d,%u,%u,%d,%d,%ld,%ld,%u,%u\n",
static_cast<unsigned long>(latest.epoch_secs),
static_cast<unsigned long>(latest.uptime_secs),
static_cast<unsigned>(latest.battery_mv),
@@ -838,7 +1019,18 @@ void StatsHistory::flushSummaryLog() {
static_cast<unsigned>(latest.neighbour_count),
static_cast<unsigned>(latest.direct_dups),
static_cast<unsigned>(latest.flood_dups),
static_cast<unsigned>(latest.flags));
static_cast<unsigned>(latest.flags),
static_cast<unsigned>(latest.supply_voltage_centi_v),
static_cast<int>(latest.sensor_temp_deci_c),
static_cast<int>(latest.mcu_temp_deci_c),
static_cast<unsigned>(latest.humidity_deci_pct),
static_cast<unsigned>(latest.pressure_deci_hpa),
static_cast<int>(latest.pressure_altitude_m),
static_cast<int>(latest.gps_altitude_m),
static_cast<long>(latest.gps_lat_e6),
static_cast<long>(latest.gps_lon_e6),
static_cast<unsigned>(latest.sensor_flags),
static_cast<unsigned>(latest.gps_satellites));
const size_t written = file.print(line);
file.flush();
file.close();
@@ -943,7 +1135,7 @@ bool StatsHistory::buildSeriesJson(const char* series, char* buffer, size_t buff
if (_sample_count == 0) {
snprintf(buffer, buffer_size,
"{\"series\":\"%s\",\"title\":\"%s\",\"unit\":\"%s\",\"interval_secs\":%lu,\"current\":0,\"oldest_age_secs\":0,\"latest_age_secs\":0,\"points\":[]}",
"{\"series\":\"%s\",\"title\":\"%s\",\"unit\":\"%s\",\"interval_secs\":%lu,\"current\":null,\"oldest_age_secs\":0,\"latest_age_secs\":0,\"points\":[]}",
series,
seriesTitle(series),
seriesUnit(series),
@@ -959,55 +1151,83 @@ bool StatsHistory::buildSeriesJson(const char* series, char* buffer, size_t buff
HistorySample previous{};
bool have_previous = false;
int current_value = 0;
offset += snprintf(&buffer[offset], buffer_size - offset,
"{\"series\":\"%s\",\"title\":\"%s\",\"unit\":\"%s\",\"interval_secs\":%lu,\"current\":",
series,
seriesTitle(series),
seriesUnit(series),
static_cast<unsigned long>(kSampleIntervalSecs));
bool have_current_value = false;
getSampleFromOldest(_sample_count - 1, sample);
if (strcmp(series, "packets") == 0 && _sample_count >= 2) {
getSampleFromOldest(_sample_count - 2, previous);
current_value = buildPointValue(sample, &previous, series);
have_current_value = buildPointValue(sample, &previous, series, current_value);
} else {
current_value = buildPointValue(sample, nullptr, series);
have_current_value = buildPointValue(sample, nullptr, series, current_value);
}
size_t last_index = 0;
for (size_t i = 0, emitted = 0; i < _sample_count && emitted < points; i += step, ++emitted) {
last_index = i;
}
HistorySample oldest_sample{};
HistorySample latest_emitted_sample{};
const bool have_oldest_sample = getSampleFromOldest(0, oldest_sample);
const bool have_latest_emitted_sample = getSampleFromOldest(last_index, latest_emitted_sample);
const uint32_t oldest_age_secs = have_oldest_sample ? sampleAgeSecs(oldest_sample, now_epoch_secs, now_uptime_secs) : 0;
const uint32_t latest_age_secs = have_latest_emitted_sample ? sampleAgeSecs(latest_emitted_sample, now_epoch_secs, now_uptime_secs) : 0;
offset += snprintf(&buffer[offset], buffer_size - offset,
"%d,\"oldest_age_secs\":%lu,\"latest_age_secs\":%lu,\"points\":[",
current_value,
static_cast<unsigned long>(oldest_age_secs),
static_cast<unsigned long>(latest_age_secs));
bool have_oldest_sample = false;
bool have_latest_emitted_sample = false;
size_t emitted = 0;
for (size_t i = 0; i < _sample_count && emitted < points; i += step, ++emitted) {
if (!getSampleFromOldest(i, sample)) {
break;
}
int value = buildPointValue(sample, have_previous ? &previous : nullptr, series);
offset += snprintf(&buffer[offset], buffer_size - offset,
"%s[%lu,%d]",
emitted == 0 ? "" : ",",
static_cast<unsigned long>(sample.uptime_secs),
value);
int value = 0;
const bool have_value = buildPointValue(sample, have_previous ? &previous : nullptr, series, value);
if (have_value) {
if (!have_oldest_sample) {
oldest_sample = sample;
have_oldest_sample = true;
}
latest_emitted_sample = sample;
have_latest_emitted_sample = true;
}
previous = sample;
have_previous = true;
if (offset + 24 >= buffer_size) {
}
const uint32_t oldest_age_secs = have_oldest_sample ? sampleAgeSecs(oldest_sample, now_epoch_secs, now_uptime_secs) : 0;
const uint32_t latest_age_secs = have_latest_emitted_sample ? sampleAgeSecs(latest_emitted_sample, now_epoch_secs, now_uptime_secs) : 0;
char current_value_buf[24];
const char* current_json = "null";
if (have_current_value) {
snprintf(current_value_buf, sizeof(current_value_buf), "%d", current_value);
current_json = current_value_buf;
}
const int header_written = snprintf(buffer, buffer_size,
"{\"series\":\"%s\",\"title\":\"%s\",\"unit\":\"%s\",\"interval_secs\":%lu,\"current\":%s,\"oldest_age_secs\":%lu,\"latest_age_secs\":%lu,\"points\":[",
series,
seriesTitle(series),
seriesUnit(series),
static_cast<unsigned long>(kSampleIntervalSecs),
current_json,
static_cast<unsigned long>(oldest_age_secs),
static_cast<unsigned long>(latest_age_secs));
if (header_written < 0 || static_cast<size_t>(header_written) >= buffer_size) {
return false;
}
offset = static_cast<size_t>(header_written);
emitted = 0;
size_t valid_emitted = 0;
have_previous = false;
for (size_t i = 0; i < _sample_count && emitted < points; i += step, ++emitted) {
if (!getSampleFromOldest(i, sample)) {
break;
}
int value = 0;
const bool have_value = buildPointValue(sample, have_previous ? &previous : nullptr, series, value);
if (have_value) {
offset += snprintf(&buffer[offset], buffer_size - offset,
"%s[%lu,%d]",
valid_emitted == 0 ? "" : ",",
static_cast<unsigned long>(sample.uptime_secs),
value);
valid_emitted++;
if (offset + 24 >= buffer_size) {
break;
}
}
previous = sample;
have_previous = true;
}
snprintf(&buffer[offset], buffer_size - offset, "]}");
+28 -1
Wyświetl plik
@@ -24,9 +24,20 @@ struct HistorySample {
int16_t last_rssi_x4;
int16_t last_snr_x4;
int16_t noise_floor;
uint16_t supply_voltage_centi_v;
int16_t sensor_temp_deci_c;
int16_t mcu_temp_deci_c;
uint16_t humidity_deci_pct;
uint16_t pressure_deci_hpa;
int16_t pressure_altitude_m;
int16_t gps_altitude_m;
int32_t gps_lat_e6;
int32_t gps_lon_e6;
uint16_t sensor_flags;
uint8_t gps_satellites;
uint8_t flags;
int8_t battery_pct;
uint16_t reserved;
uint8_t reserved;
};
struct HistoryEvent {
@@ -48,6 +59,22 @@ enum HistorySampleFlags : uint8_t {
HISTORY_FLAG_ARCHIVE_MOUNTED = 1 << 7,
};
enum HistorySampleSensorFlags : uint16_t {
HISTORY_SENSOR_SUPPLY_VOLTAGE = 1 << 0,
HISTORY_SENSOR_TEMP = 1 << 1,
HISTORY_SENSOR_MCU_TEMP = 1 << 2,
HISTORY_SENSOR_HUMIDITY = 1 << 3,
HISTORY_SENSOR_PRESSURE = 1 << 4,
HISTORY_SENSOR_PRESSURE_ALTITUDE = 1 << 5,
HISTORY_SENSOR_GPS_PRESENT = 1 << 6,
HISTORY_SENSOR_GPS_ENABLED = 1 << 7,
HISTORY_SENSOR_GPS_FIX = 1 << 8,
HISTORY_SENSOR_GPS_LAT = 1 << 9,
HISTORY_SENSOR_GPS_LON = 1 << 10,
HISTORY_SENSOR_GPS_ALTITUDE = 1 << 11,
HISTORY_SENSOR_GPS_SATELLITES = 1 << 12,
};
enum HistoryEventType : uint8_t {
HISTORY_EVENT_BOOT = 1,
HISTORY_EVENT_WEB_STARTED = 2,
+95 -23
Wyświetl plik
@@ -484,6 +484,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
.meter-fill.warn { background:linear-gradient(90deg,#d7a531,#e9bf52); }
.meter-fill.bad { background:linear-gradient(90deg,#bf4b4b,#dd6a6a); }
.metric-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
.metric-grid-4 { grid-template-columns:repeat(4,minmax(0,1fr)); }
.core-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
.core-grid .hud-row { align-content:start; }
.core-metrics { grid-template-columns:repeat(4,minmax(0,1fr)); }
@@ -1389,6 +1390,18 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
<div class="metric-value">${escapeHtml(value)}</div>
</div>`;
}
function hasMetricValue(value) {
if (value == null) return false;
if (typeof value === "number") return Number.isFinite(value);
return String(value).trim() !== "";
}
function renderMetricList(metrics, gridClass = "") {
const items = (Array.isArray(metrics) ? metrics : []).filter((item) => item && hasMetricValue(item.value));
if (!items.length) return "";
const classes = ["metric-grid"];
if (gridClass) classes.push(gridClass);
return `<div class="${classes.join(" ")}">${items.map((item) => renderMetric(item.label, item.value)).join("")}</div>`;
}
function renderMissingCard(title, message) {
return `<section class="hud-card">
<h3>${escapeHtml(title)}</h3>
@@ -1575,20 +1588,44 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const archiveUsed = archive.used_bytes || 0;
const archiveFree = Math.max(0, archiveTotal - archiveUsed);
const archiveFreePct = archiveTotal > 0 ? Math.round((archiveFree / archiveTotal) * 100) : 0;
const archiveMetrics = archive.available ? `
${renderMetric("Card", archive.type || "--")}
${renderMetric("Archive Total", formatArchiveGigabytes(archiveTotal))}
${renderMetric("Archive Used", formatArchiveUsed(archiveUsed))}
${renderMetric("Archive Free", archiveTotal > 0 ? (formatArchiveGigabytes(archiveFree) + " (" + archiveFreePct + "%)") : "--")}` : "";
const metrics = [
{ label:"MQTT", value:services.mqtt_state || (services.mqtt_connected ? "up" : "down") },
{ label:"Web", value:services.web_panel_up ? services.web_auth || "up" : "down" },
{ label:"Archive", value:archive.available ? archive.logical || "archive" : "unavailable" },
{ label:"Neighbours", value:packets.neighbors || 0 }
];
if (archive.available) {
metrics.push(
{ label:"Card", value:archive.type || "--" },
{ label:"Archive Total", value:formatArchiveGigabytes(archiveTotal) },
{ label:"Archive Used", value:formatArchiveUsed(archiveUsed) },
{ label:"Archive Free", value:archiveTotal > 0 ? (formatArchiveGigabytes(archiveFree) + " (" + archiveFreePct + "%)") : "--" }
);
}
return `<section class="hud-card">
<h3>Services</h3>
<div class="metric-grid">
${renderMetric("MQTT", services.mqtt_state || (services.mqtt_connected ? "up" : "down"))}
${renderMetric("Web", services.web_panel_up ? services.web_auth || "up" : "down")}
${renderMetric("Archive", archive.available ? archive.logical || "archive" : "unavailable")}
${renderMetric("Neighbours", packets.neighbors || 0)}
${archiveMetrics}
</div>
${renderMetricList(metrics, "metric-grid-4")}
</section>`;
}
function renderSensorsCard(sensorSummary) {
const sensors = sensorSummary && typeof sensorSummary === "object" ? sensorSummary : {};
const metrics = [
Number.isFinite(sensors.supply_voltage_v) ? { label:"Voltage", value:sensors.supply_voltage_v.toFixed(2) + " V" } : null,
Number.isFinite(sensors.sensor_temp_c) ? { label:"Sensor Temp", value:sensors.sensor_temp_c.toFixed(1) + " C" } : null,
Number.isFinite(sensors.humidity_pct) ? { label:"Humidity", value:Math.round(sensors.humidity_pct) + " %" } : null,
Number.isFinite(sensors.pressure_hpa) ? { label:"Barometer", value:sensors.pressure_hpa.toFixed(1) + " hPa" } : null,
Number.isFinite(sensors.pressure_altitude_m) ? { label:"Pressure Altitude", value:Math.round(sensors.pressure_altitude_m) + " m" } : null,
Number.isFinite(sensors.mcu_temp_c) ? { label:"MCU Temp", value:sensors.mcu_temp_c.toFixed(1) + " C" } : null,
typeof sensors.gps_enabled === "boolean" ? { label:"GPS", value:sensors.gps_fix ? "Fix" : (sensors.gps_enabled ? "Searching" : "Off") } : null,
Number.isFinite(sensors.satellites) ? { label:"Satellites", value:String(Math.round(sensors.satellites)) } : null,
Number.isFinite(sensors.gps_lat) ? { label:"Latitude", value:sensors.gps_lat.toFixed(6) } : null,
Number.isFinite(sensors.gps_lon) ? { label:"Longitude", value:sensors.gps_lon.toFixed(6) } : null,
Number.isFinite(sensors.gps_altitude_m) ? { label:"GPS Altitude", value:Math.round(sensors.gps_altitude_m) + " m" } : null
];
if (!renderMetricList(metrics)) return "";
return `<section class="hud-card">
<h3>Environment</h3>
${renderMetricList(metrics, "metric-grid-4")}
</section>`;
}
function renderEventsSection(events) {
@@ -1668,6 +1705,14 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
renderStatsDashboard(results, [], "statsSummary");
const summaryEl = document.getElementById("statsSummary");
if (!summaryEl) return;
const sensorCards = [
renderSensorsCard(payload && payload.sensors ? payload.sensors : null)
].filter((card) => card);
if (sensorCards.length > 0) {
sensorCards.forEach((card) => {
summaryEl.innerHTML += `<div class="hud-grid-1">${card}</div>`;
});
}
summaryEl.innerHTML += `<div class="hud-grid-1">${renderServicesCard(payload)}</div>`;
const introEl = document.getElementById("statsTrendIntro");
if (introEl) {
@@ -1705,10 +1750,16 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
function formatTrendValue(key, value) {
if (!Number.isFinite(value)) return "--";
if (key === "battery") return Math.round(value) + " mV";
if (key === "voltage") return (value / 100).toFixed(2) + " V";
if (key === "memory") return formatBytes(value);
if (key === "packets") return Math.round(value) + " pkts";
if (key === "signal") return (value / 4).toFixed(1) + " dBm";
if (key === "noise_floor") return (value / 4).toFixed(1) + " dBm";
if (key === "sensor_temp" || key === "mcu_temp") return (value / 10).toFixed(1) + " C";
if (key === "humidity") return (value / 10).toFixed(1) + " %";
if (key === "pressure") return (value / 10).toFixed(1) + " hPa";
if (key === "pressure_altitude" || key === "gps_altitude") return Math.round(value) + " m";
if (key === "gps_satellites") return Math.round(value) + " sats";
return String(value);
}
function formatArchiveGigabytes(value) {
@@ -1935,10 +1986,20 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
axisRightEl.style.color = "var(--status-red)";
}
}
function initTrendCards() {
function getTrendSeriesOrder(summaryPayload) {
const sensors = summaryPayload && summaryPayload.sensors ? summaryPayload.sensors : null;
const gpsEnabled = !!(sensors && sensors.gps_enabled === true);
const order = ["battery", "memory", "signal", "noise_floor", "packets"];
if (gpsEnabled) {
order.push("gps_satellites");
}
return order.concat(["voltage", "sensor_temp", "humidity", "pressure", "pressure_altitude", "mcu_temp", "gps_altitude"]);
}
function initTrendCards(seriesOrder) {
const trendsEl = document.getElementById("statsTrends");
if (!trendsEl) return;
trendsEl.innerHTML = ["battery", "memory", "signal", "noise_floor", "packets"].map((key) =>
const order = Array.isArray(seriesOrder) && seriesOrder.length ? seriesOrder : getTrendSeriesOrder(null);
trendsEl.innerHTML = order.map((key) =>
`<section class="trend-card" id="trend-${key}">
<div class="trend-title">${escapeHtml(key)}</div>
<div class="spark-axis"><span></span><span>Loading...</span></div>
@@ -2248,17 +2309,18 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const summaryEl = document.getElementById("statsSummary");
if (!summaryEl) return;
summaryEl.innerHTML = '<div class="stats-empty">Loading summary...</div>';
initTrendCards();
let summaryPayload = null;
try {
const payload = await fetchJson("/api/stats?view=summary");
if (!payload) throw new Error("no summary payload");
renderStatsSummary(payload);
summaryPayload = await fetchJson("/api/stats?view=summary");
if (!summaryPayload) throw new Error("no summary payload");
renderStatsSummary(summaryPayload);
initTrendCards(getTrendSeriesOrder(summaryPayload));
} catch (error) {
summaryEl.innerHTML = `<div class="stats-error">${escapeHtml(error && error.message ? error.message : "summary unavailable")}</div>`;
return;
}
const seriesOrder = ["battery", "memory", "signal", "noise_floor", "packets"];
const seriesOrder = getTrendSeriesOrder(summaryPayload);
for (const key of seriesOrder) {
try {
const payload = await fetchJson("/api/stats?series=" + encodeURIComponent(key));
@@ -2577,10 +2639,7 @@ bool WebPanelServer::start() {
}
void WebPanelServer::stop() {
if (_redirect_server != nullptr) {
httpd_stop(_redirect_server);
_redirect_server = nullptr;
}
stopRedirectServer();
if (_server != nullptr) {
WEB_PANEL_LOG("server stopped");
httpd_ssl_stop(_server);
@@ -2598,6 +2657,14 @@ bool WebPanelServer::hasSessionToken() const {
return _token[0] != 0;
}
void WebPanelServer::stopRedirectServer() {
if (_redirect_server != nullptr) {
WEB_PANEL_LOG("redirect server stopped");
httpd_stop(_redirect_server);
_redirect_server = nullptr;
}
}
bool WebPanelServer::shouldAutoLock(unsigned long now_ms) const {
if (_server == nullptr || _token[0] == 0 || kWebIdleTimeoutMs == 0 || _last_activity_ms == 0) {
return false;
@@ -2717,6 +2784,11 @@ esp_err_t WebPanelServer::handleCommand(httpd_req_t* req) {
ctx->self->noteActivity();
memset(reply, 0, kWebReplyBufferSize);
if (strcmp(command, "start ota") == 0) {
// OTA serves its own HTTP listener on port 80, so release the
// web-panel redirect listener first or it will keep owning that port.
ctx->self->stopRedirectServer();
}
ctx->self->_runner->runWebCommand(command, reply, kWebReplyBufferSize);
httpd_resp_set_type(req, "text/plain; charset=utf-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
+1
Wyświetl plik
@@ -71,6 +71,7 @@ private:
void refreshToken();
bool isAuthorized(httpd_req_t* req) const;
void noteActivity();
void stopRedirectServer();
#else
WebPanelCommandRunner* _runner;
#endif