feat(build): gzip страниц веб-панели и проверка размера образа; модули AdvertJail и ChannelBudget
This commit is contained in:
1 parent
a4e80805d6
commit
94d2ae57f1
7 files changed
+292
-4
No files matched your search
@@ -395,6 +395,12 @@ MQTT-репитер использует:
|
|||||||
- MQTT-целей стало 112 (42 uplink, 34 espnow, 36 bridge); после слияния проверены сборкой `heltec_v4_repeater_mqtt`, `LilyGo_TBeam_1W_repeater_mqtt`, `Meshadventurer_sx1262_repeater_mqtt`, `RAK_3112_repeater_mqtt`, `heltec_v4_repeater_mqtt_espnow`, `heltec_v4_repeater_mqtt_bridge`, `LilyGo_Tlora_C6_repeater_mqtt_espnow`, `LilyGo_TBeam_1W_repeater_mqtt_espnow`, `Waveshare_S3_LR1121_repeater_mqtt`, `Waveshare_S3_LR1121_repeater_mqtt_bridge`;
|
- MQTT-целей стало 112 (42 uplink, 34 espnow, 36 bridge); после слияния проверены сборкой `heltec_v4_repeater_mqtt`, `LilyGo_TBeam_1W_repeater_mqtt`, `Meshadventurer_sx1262_repeater_mqtt`, `RAK_3112_repeater_mqtt`, `heltec_v4_repeater_mqtt_espnow`, `heltec_v4_repeater_mqtt_bridge`, `LilyGo_Tlora_C6_repeater_mqtt_espnow`, `LilyGo_TBeam_1W_repeater_mqtt_espnow`, `Waveshare_S3_LR1121_repeater_mqtt`, `Waveshare_S3_LR1121_repeater_mqtt_bridge`;
|
||||||
- в `AGENTS.md` обновлён процесс: работаем в `main` (ветка `develop` — устаревшая, от эпохи EastMesh), upstream тянем релизным тегом, а не веткой `dev`, версия upstream — `v1.18.0`.
|
- в `AGENTS.md` обновлён процесс: работаем в `main` (ветка `develop` — устаревшая, от эпохи EastMesh), upstream тянем релизным тегом, а не веткой `dev`, версия upstream — `v1.18.0`.
|
||||||
|
|
||||||
|
**Сжатие веб-панели и проверка размера образа**
|
||||||
|
|
||||||
|
- страницы панели сжимаются gzip во время сборки (rch/esp32/extra_scripts/gzip_web_panel.py): 153 819 байт текста превращаются в 33 054 байта, образ репитера стал легче примерно на 118 КБ, браузер распаковывает страницы сам по Content-Encoding: gzip;
|
||||||
|
- добавлена проверка rch/esp32/extra_scripts/check_app_size.py: сборка падает, если образ больше своего app-раздела (PlatformIO считает только секции ELF и не учитывает выравнивание);
|
||||||
|
- в репозиторий добавлены модули из форка MeshCoreTel-moe: src/helpers/AdvertJail.h (лимит флуда анонсами по отправителю) и src/helpers/ChannelBudget.h (бюджет эфира на канал); подключение к обработке пакетов — следующий шаг.
|
||||||
|
|
||||||
**Исправлено**
|
**Исправлено**
|
||||||
|
|
||||||
- в MQTT-телеметрию уходила только база версии (`v1.18.0`), потому что поле `firmware_version` бралось из `FIRMWARE_VERSION`, куда при сборке передаётся версия upstream. Теперь строка собирается в коде: `MQTT_FIRMWARE_VERSION = FIRMWARE_VERSION "-posadmesh-meshcoretel"`, то есть публикуется `v1.18.0-posadmesh-meshcoretel` — одинаково при локальной сборке и в CI;
|
- в MQTT-телеметрию уходила только база версии (`v1.18.0`), потому что поле `firmware_version` бралось из `FIRMWARE_VERSION`, куда при сборке передаётся версия upstream. Теперь строка собирается в коде: `MQTT_FIRMWARE_VERSION = FIRMWARE_VERSION "-posadmesh-meshcoretel"`, то есть публикуется `v1.18.0-posadmesh-meshcoretel` — одинаково при локальной сборке и в CI;
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
Import("env")
|
||||||
|
|
||||||
|
# Падаем на сборке, если образ прошивки больше своего app-раздела.
|
||||||
|
# PlatformIO показывает "Flash: N%" только по секциям ELF, а в образе ещё есть выравнивание
|
||||||
|
# (на ESP32-C6 это около 70 КБ): загрузчик отказывается стартовать образ длиннее раздела
|
||||||
|
# ("Image length ... doesn't fit in partition length ..."), и OTA его тоже не запишет.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_size(value):
|
||||||
|
value = value.strip()
|
||||||
|
if value[-1:].upper() in ("K", "M"):
|
||||||
|
return int(value[:-1], 0) * (1024 if value[-1].upper() == "K" else 1024 * 1024)
|
||||||
|
return int(value, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _smallest_app_partition(partitions_csv):
|
||||||
|
sizes = []
|
||||||
|
with open(partitions_csv) as fp:
|
||||||
|
for line in fp:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
tokens = [t.strip() for t in line.split(",")]
|
||||||
|
if len(tokens) >= 5 and tokens[1] in ("app", "0"):
|
||||||
|
sizes.append(_parse_size(tokens[4]))
|
||||||
|
return min(sizes) if sizes else None
|
||||||
|
|
||||||
|
|
||||||
|
def check_app_size(source, target, env):
|
||||||
|
image = target[0].get_abspath()
|
||||||
|
partitions_csv = env.subst("$PARTITIONS_TABLE_CSV")
|
||||||
|
if not os.path.isfile(image) or not os.path.isfile(partitions_csv):
|
||||||
|
return 0
|
||||||
|
slot = _smallest_app_partition(partitions_csv)
|
||||||
|
if slot is None:
|
||||||
|
return 0
|
||||||
|
size = os.path.getsize(image)
|
||||||
|
if size > slot:
|
||||||
|
sys.stderr.write(
|
||||||
|
"Error: %s is %d bytes, %d more than its %d-byte app partition (%s); "
|
||||||
|
"the bootloader will not start it\n"
|
||||||
|
% (os.path.basename(image), size, size - slot, slot, os.path.basename(partitions_csv))
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
print("App image %d of %d bytes, %d left" % (size, slot, slot - size))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", check_app_size)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
Import("env")
|
||||||
|
|
||||||
|
# Сжимаем страницы веб-панели прямо во время сборки: в прошивке они лежат gzip-архивами
|
||||||
|
# (HTML/JS ~146 КБ превращаются примерно в 30 КБ), а браузер распаковывает их сам по
|
||||||
|
# заголовку Content-Encoding: gzip. Исходники остаются читаемыми строками в
|
||||||
|
# WebPanelServer.cpp, скрипт только генерирует $BUILD_DIR/generated/WebPanelAssets.h
|
||||||
|
# и определяет WEB_PANEL_GZIP.
|
||||||
|
|
||||||
|
import gzip
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PAGE = re.compile(r'const char (\w+)\[\] PROGMEM = R"HTML\((.*?)\)HTML";', re.S)
|
||||||
|
WANTED = ["kWebPanelLoginHtml", "kWebPanelStatsDisabledHtml", "kWebPanelAppHtml"]
|
||||||
|
|
||||||
|
|
||||||
|
def render(name, data):
|
||||||
|
rows = ",\n".join(
|
||||||
|
" " + ",".join("0x%02x" % b for b in data[i:i + 20]) for i in range(0, len(data), 20)
|
||||||
|
)
|
||||||
|
return "static const uint8_t %sGz[%d] = {\n%s\n};\n" % (name, len(data), rows)
|
||||||
|
|
||||||
|
|
||||||
|
source = Path(env["PROJECT_DIR"]) / "src/helpers/web/WebPanelServer.cpp"
|
||||||
|
text = source.read_text(encoding="utf-8")
|
||||||
|
pages = {name: body for name, body in PAGE.findall(text)}
|
||||||
|
|
||||||
|
missing = [name for name in WANTED if name not in pages]
|
||||||
|
if missing:
|
||||||
|
print("gzip_web_panel: страницы не найдены (%s), панель останется несжатой" % ", ".join(missing))
|
||||||
|
else:
|
||||||
|
header = (
|
||||||
|
"// Сгенерировано arch/esp32/extra_scripts/gzip_web_panel.py из WebPanelServer.cpp.\n"
|
||||||
|
"// Не редактировать: файл пересобирается при каждой сборке.\n"
|
||||||
|
"#pragma once\n\n#include <stdint.h>\n\n"
|
||||||
|
+ "\n".join(render(name, gzip.compress(pages[name].encode("utf-8"), 9, mtime=0)) for name in WANTED)
|
||||||
|
)
|
||||||
|
out_dir = Path(env.subst("$BUILD_DIR")) / "generated"
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
out = out_dir / "WebPanelAssets.h"
|
||||||
|
# перезаписываем только при изменении, иначе WebPanelServer.cpp пересобирался бы каждый раз
|
||||||
|
if not out.exists() or out.read_text(encoding="utf-8") != header:
|
||||||
|
out.write_text(header, encoding="utf-8")
|
||||||
|
env.Append(CPPPATH=[str(out_dir)], CPPDEFINES=[("WEB_PANEL_GZIP", 1)])
|
||||||
|
total_raw = sum(len(pages[name]) for name in WANTED)
|
||||||
|
print("gzip_web_panel: %d страниц, %d байт текста сжато в %d" % (
|
||||||
|
len(WANTED), total_raw, sum(len(gzip.compress(pages[name].encode("utf-8"), 9, mtime=0)) for name in WANTED)))
|
||||||
@@ -63,7 +63,9 @@ monitor_filters = esp32_exception_decoder
|
|||||||
extra_scripts =
|
extra_scripts =
|
||||||
pre:arch/esp32/extra_scripts/generate_web_panel_cert.py
|
pre:arch/esp32/extra_scripts/generate_web_panel_cert.py
|
||||||
pre:arch/esp32/extra_scripts/git_commit_hash.py
|
pre:arch/esp32/extra_scripts/git_commit_hash.py
|
||||||
|
pre:arch/esp32/extra_scripts/gzip_web_panel.py
|
||||||
merge-bin.py
|
merge-bin.py
|
||||||
|
post:arch/esp32/extra_scripts/check_app_size.py
|
||||||
build_flags = ${arduino_base.build_flags}
|
build_flags = ${arduino_base.build_flags}
|
||||||
-D ESP32_PLATFORM
|
-D ESP32_PLATFORM
|
||||||
; -D ESP32_CPU_FREQ=80 ; change it to your need
|
; -D ESP32_CPU_FREQ=80 ; change it to your need
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define ADVERT_JAIL_KEY_SIZE 8 // public key prefix: long enough that nobody grinds a lookalike key
|
||||||
|
#define ADVERT_JAIL_THRESHOLD 6 // strikes that put a sender in jail
|
||||||
|
|
||||||
|
struct AdvertJailEntry {
|
||||||
|
uint8_t key[ADVERT_JAIL_KEY_SIZE];
|
||||||
|
uint32_t first_seen; // millis() of the first counted advert
|
||||||
|
uint32_t last_seen; // millis() of the latest counted advert, 0 = free slot
|
||||||
|
uint32_t last_advert_time; // the sender's own timestamp on that advert
|
||||||
|
uint16_t adverts; // adverts counted
|
||||||
|
uint8_t strikes; // adverts that came sooner than the interval, capped at the threshold
|
||||||
|
uint8_t jailed;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace mesh {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Per-sender flood advert limit, ported from MeshSydney's advert.jail.
|
||||||
|
* An advert that comes sooner than 'interval_ms' after the sender's previous one is a strike;
|
||||||
|
* ADVERT_JAIL_THRESHOLD strikes put the sender in jail, and while jailed its early adverts are
|
||||||
|
* dropped. An advert that keeps to the interval takes a strike off; below 2 strikes the sender
|
||||||
|
* is let out. The same advert seen again (same timestamp) is not counted.
|
||||||
|
* \param table 'size' entries, zero-filled when empty
|
||||||
|
* \param pub_key the sender's public key (the first ADVERT_JAIL_KEY_SIZE bytes are kept)
|
||||||
|
* \param advert_time the timestamp inside the advert, by the sender's clock
|
||||||
|
* \param now_ms a millisecond clock (wrap-around is fine)
|
||||||
|
* \returns true if this advert must not be forwarded
|
||||||
|
*/
|
||||||
|
inline bool checkAdvertJail(AdvertJailEntry* table, int size, const uint8_t* pub_key, uint32_t advert_time,
|
||||||
|
uint32_t now_ms, uint32_t interval_ms) {
|
||||||
|
if (now_ms == 0) now_ms = 1; // 0 marks a free slot
|
||||||
|
|
||||||
|
// find the sender, else the slot to reuse: a free one, then the fewest strikes, then the longest unheard
|
||||||
|
AdvertJailEntry* entry = NULL;
|
||||||
|
AdvertJailEntry* evict = &table[0];
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
AdvertJailEntry* e = &table[i];
|
||||||
|
if (e->last_seen != 0 && memcmp(e->key, pub_key, ADVERT_JAIL_KEY_SIZE) == 0) {
|
||||||
|
entry = e;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (evict->last_seen == 0) continue;
|
||||||
|
if (e->last_seen == 0 || e->strikes < evict->strikes
|
||||||
|
|| (e->strikes == evict->strikes && now_ms - e->last_seen > now_ms - evict->last_seen)) {
|
||||||
|
evict = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry == NULL) { // first advert from this sender
|
||||||
|
memset(evict, 0, sizeof(*evict));
|
||||||
|
memcpy(evict->key, pub_key, ADVERT_JAIL_KEY_SIZE);
|
||||||
|
evict->first_seen = evict->last_seen = now_ms;
|
||||||
|
evict->last_advert_time = advert_time;
|
||||||
|
evict->adverts = 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (advert_time == entry->last_advert_time) { // the same advert again
|
||||||
|
return entry->jailed != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool interval_ok = now_ms - entry->last_seen >= interval_ms;
|
||||||
|
if (interval_ok && entry->strikes > 0) entry->strikes--;
|
||||||
|
if (entry->jailed && entry->strikes < 2) entry->jailed = 0;
|
||||||
|
|
||||||
|
entry->last_seen = now_ms;
|
||||||
|
entry->last_advert_time = advert_time;
|
||||||
|
if (entry->adverts < 0xFFFF) entry->adverts++;
|
||||||
|
|
||||||
|
if (!interval_ok && entry->strikes < ADVERT_JAIL_THRESHOLD) entry->strikes++;
|
||||||
|
if (!entry->jailed && entry->strikes >= ADVERT_JAIL_THRESHOLD) entry->jailed = 1;
|
||||||
|
|
||||||
|
return entry->jailed && !interval_ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define CHAN_BUDGET_CHANNELS 256 // one slot per 1-byte channel hash
|
||||||
|
#define CHAN_BUDGET_WINDOW_MS (10UL * 60UL * 1000UL) // the budget is per 10 minutes
|
||||||
|
#define CHAN_BUDGET_MAX 1000
|
||||||
|
|
||||||
|
namespace mesh {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Per-channel budget for flood group packets (GRP_TXT, GRP_DATA), keyed by the 1-byte channel hash.
|
||||||
|
* A token bucket kept in GCRA form (one "theoretical arrival time" per channel): a channel can send
|
||||||
|
* 'limit' packets back to back, then one more every window_ms / limit. A channel that never has more
|
||||||
|
* than 'limit' packets in any window_ms is never held back. Dropped packets cost nothing.
|
||||||
|
* The stored time is a fraction of the window rather than a token count, so a new limit applies
|
||||||
|
* to the same share of the window straight away.
|
||||||
|
* \param tat CHAN_BUDGET_CHANNELS entries, zero-filled at start
|
||||||
|
* \param limit packets per window, 0 = off
|
||||||
|
* \param now_ms a millisecond clock (wrap-around is fine)
|
||||||
|
* \returns true if the channel is over its budget and this packet must not be forwarded
|
||||||
|
*/
|
||||||
|
inline bool checkChannelBudget(uint32_t* tat, uint8_t channel_hash, uint32_t now_ms, uint16_t limit,
|
||||||
|
uint32_t window_ms) {
|
||||||
|
if (limit == 0) return false;
|
||||||
|
uint32_t interval = window_ms / limit;
|
||||||
|
if (interval == 0) interval = 1;
|
||||||
|
uint32_t tolerance = interval * (limit - 1); // lets 'limit' packets through back to back
|
||||||
|
|
||||||
|
int32_t ahead = (int32_t)(tat[channel_hash] - now_ms);
|
||||||
|
if (ahead <= 0 || (uint32_t)ahead > tolerance + interval) {
|
||||||
|
ahead = 0; // bucket full; a time past the bucket is left over from weeks ago (see refreshChannelBudget)
|
||||||
|
}
|
||||||
|
if ((uint32_t)ahead > tolerance) return true; // bucket empty
|
||||||
|
|
||||||
|
tat[channel_hash] = now_ms + (uint32_t)ahead + interval;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Call every few minutes: moves the stored time of idle channels up to now, so that it never gets old
|
||||||
|
* enough (~49 days of a 32-bit millisecond clock) to wrap around into the window and hold back
|
||||||
|
* a channel that has been quiet.
|
||||||
|
*/
|
||||||
|
inline void refreshChannelBudget(uint32_t* tat, uint32_t now_ms, uint32_t window_ms) {
|
||||||
|
for (int h = 0; h < CHAN_BUDGET_CHANNELS; h++) {
|
||||||
|
int32_t ahead = (int32_t)(tat[h] - now_ms);
|
||||||
|
if (ahead <= 0 || (uint32_t)ahead > window_ms) tat[h] = now_ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \returns how much of the channel's budget is in use: 0 (idle) .. limit (over budget, packets are dropped)
|
||||||
|
*/
|
||||||
|
inline uint16_t getChannelBudgetUsed(const uint32_t* tat, uint8_t channel_hash, uint32_t now_ms, uint16_t limit,
|
||||||
|
uint32_t window_ms) {
|
||||||
|
if (limit == 0) return 0;
|
||||||
|
uint32_t interval = window_ms / limit;
|
||||||
|
if (interval == 0) interval = 1;
|
||||||
|
|
||||||
|
int32_t ahead = (int32_t)(tat[channel_hash] - now_ms);
|
||||||
|
if (ahead <= 0 || (uint32_t)ahead > interval * limit) return 0;
|
||||||
|
return (uint16_t)(((uint32_t)ahead + interval - 1) / interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,6 +12,11 @@
|
|||||||
#include <Update.h>
|
#include <Update.h>
|
||||||
#include "../mqtt/generated/WebPanelCert.h"
|
#include "../mqtt/generated/WebPanelCert.h"
|
||||||
|
|
||||||
|
#if defined(WEB_PANEL_GZIP)
|
||||||
|
// Страницы, сжатые gzip во время сборки (arch/esp32/extra_scripts/gzip_web_panel.py)
|
||||||
|
#include <WebPanelAssets.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
#ifndef WEB_PANEL_STACK_SIZE
|
#ifndef WEB_PANEL_STACK_SIZE
|
||||||
@@ -120,6 +125,37 @@ esp_err_t sendWhole(httpd_req_t* req, const char* text, const size_t len) {
|
|||||||
return httpd_resp_send_chunk(req, nullptr, 0);
|
return httpd_resp_send_chunk(req, nullptr, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отдача сжатой страницы: браузер распаковывает её сам по Content-Encoding: gzip,
|
||||||
|
// поэтому страницы панели занимают в прошивке вчетверо меньше места.
|
||||||
|
esp_err_t sendGzipped(httpd_req_t* req, const uint8_t* data, const size_t len) {
|
||||||
|
if (data == nullptr || len == 0) {
|
||||||
|
return httpd_resp_send_chunk(req, nullptr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
|
||||||
|
|
||||||
|
for (size_t offset = 0; offset < len; offset += kWebPageChunkSize) {
|
||||||
|
const size_t chunk = ((len - offset) < kWebPageChunkSize) ? (len - offset) : kWebPageChunkSize;
|
||||||
|
if (httpd_resp_send_chunk(req, reinterpret_cast<const char*>(data + offset), chunk) != ESP_OK) {
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpd_resp_send_chunk(req, nullptr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(WEB_PANEL_GZIP)
|
||||||
|
#define sendLoginPage(req) sendGzipped(req, kWebPanelLoginHtmlGz, sizeof(kWebPanelLoginHtmlGz))
|
||||||
|
#define sendStatsDisabledPage(req) sendGzipped(req, kWebPanelStatsDisabledHtmlGz, sizeof(kWebPanelStatsDisabledHtmlGz))
|
||||||
|
#define sendAppPage(req) sendGzipped(req, kWebPanelAppHtmlGz, sizeof(kWebPanelAppHtmlGz))
|
||||||
|
#else
|
||||||
|
#define sendLoginPage(req) sendProgmem(req, kWebPanelLoginHtml)
|
||||||
|
#define sendStatsDisabledPage(req) sendProgmem(req, kWebPanelStatsDisabledHtml)
|
||||||
|
#define sendAppPage(req) sendProgmem(req, kWebPanelAppHtml)
|
||||||
|
#endif
|
||||||
|
|
||||||
esp_err_t sendJsonEscapedChunk(httpd_req_t* req, const char* src) {
|
esp_err_t sendJsonEscapedChunk(httpd_req_t* req, const char* src) {
|
||||||
char chunk[48];
|
char chunk[48];
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
@@ -3534,7 +3570,7 @@ esp_err_t WebPanelServer::handleIndex(httpd_req_t* req) {
|
|||||||
ctx->self->noteActivity();
|
ctx->self->noteActivity();
|
||||||
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
||||||
markCloseAfterSend(req);
|
markCloseAfterSend(req);
|
||||||
return finishAndClose(req, sendProgmem(req, kWebPanelLoginHtml));
|
return finishAndClose(req, sendLoginPage(req));
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t WebPanelServer::handleHttpRedirect(httpd_req_t* req) {
|
esp_err_t WebPanelServer::handleHttpRedirect(httpd_req_t* req) {
|
||||||
@@ -3561,7 +3597,7 @@ esp_err_t WebPanelServer::handleApp(httpd_req_t* req) {
|
|||||||
ctx->self->noteActivity();
|
ctx->self->noteActivity();
|
||||||
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
||||||
markCloseAfterSend(req);
|
markCloseAfterSend(req);
|
||||||
return finishAndClose(req, sendProgmem(req, kWebPanelAppHtml));
|
return finishAndClose(req, sendAppPage(req));
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t WebPanelServer::handleStatsPage(httpd_req_t* req) {
|
esp_err_t WebPanelServer::handleStatsPage(httpd_req_t* req) {
|
||||||
@@ -3573,10 +3609,10 @@ esp_err_t WebPanelServer::handleStatsPage(httpd_req_t* req) {
|
|||||||
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
httpd_resp_set_type(req, "text/html; charset=utf-8");
|
||||||
if (ctx->self->_runner != nullptr && !ctx->self->_runner->isWebStatsEnabled()) {
|
if (ctx->self->_runner != nullptr && !ctx->self->_runner->isWebStatsEnabled()) {
|
||||||
markCloseAfterSend(req);
|
markCloseAfterSend(req);
|
||||||
return finishAndClose(req, sendProgmem(req, kWebPanelStatsDisabledHtml));
|
return finishAndClose(req, sendStatsDisabledPage(req));
|
||||||
}
|
}
|
||||||
markCloseAfterSend(req);
|
markCloseAfterSend(req);
|
||||||
return finishAndClose(req, sendProgmem(req, kWebPanelAppHtml));
|
return finishAndClose(req, sendAppPage(req));
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t WebPanelServer::handleOtaRedirect(httpd_req_t* req) {
|
esp_err_t WebPanelServer::handleOtaRedirect(httpd_req_t* req) {
|
||||||
|
|||||||
Reference in new issue
Block a user