feat: add SD-backed stats archive and dedicated /stats web console

Этот коммит содержится в:
Jared Dohrman
2026-04-15 17:12:32 +10:00
родитель a1aa318352
Коммит f67aaebe7e
27 изменённых файлов: 3862 добавлений и 588 удалений
+243
Просмотреть файл
@@ -0,0 +1,243 @@
#include "ArchiveStorage.h"
#include <MeshCore.h>
#ifndef ARCHIVE_DEBUG
#if defined(MQTT_DEBUG) && MQTT_DEBUG
#define ARCHIVE_DEBUG 1
#else
#define ARCHIVE_DEBUG 0
#endif
#endif
#if ARCHIVE_DEBUG
#define ARCHIVE_LOG(fmt, ...) Serial.printf("[ARCHIVE] " fmt "\n", ##__VA_ARGS__)
#else
#define ARCHIVE_LOG(...) do { } while (0)
#endif
namespace {
#if defined(ESP32)
const char* cardTypeName(uint8_t type) {
switch (type) {
case CARD_MMC:
return "mmc";
case CARD_SD:
return "sdsc";
case CARD_SDHC:
return "sdhc";
default:
return "unknown";
}
}
#endif
} // namespace
ArchiveStorage::ArchiveStorage()
: _attempted(false), _mounted(false), _mount_failed(false), _supported(false), _card_type(0), _spi_bus(HSPI),
_cs_pin(0xFF), _sck_pin(0xFF),
_miso_pin(0xFF), _mosi_pin(0xFF), _card_size_bytes(0), _total_bytes(0), _used_bytes(0)
#if defined(ESP32)
, _spi(nullptr)
#endif
{
}
namespace {
bool mountArchiveSd(SPIClass* spi,
uint8_t spi_bus,
uint8_t cs_pin,
uint8_t sck_pin,
uint8_t miso_pin,
uint8_t mosi_pin) {
if (spi == nullptr) {
return false;
}
#if defined(P_BOARD_IMU_CS)
pinMode(P_BOARD_IMU_CS, OUTPUT);
digitalWrite(P_BOARD_IMU_CS, HIGH);
#endif
pinMode(cs_pin, OUTPUT);
digitalWrite(cs_pin, HIGH);
spi->begin(sck_pin, miso_pin, mosi_pin, cs_pin);
return SD.begin(cs_pin, *spi, 10000000U);
}
} // namespace
void ArchiveStorage::begin() {
if (_attempted) {
return;
}
_attempted = true;
#if defined(ESP32)
_supported = resolvePins();
if (!_supported) {
ARCHIVE_LOG("unsupported: no SD pin mapping");
return;
}
ARCHIVE_LOG("init bus=%u cs=%u sck=%u miso=%u mosi=%u",
static_cast<unsigned>(_spi_bus),
static_cast<unsigned>(_cs_pin),
static_cast<unsigned>(_sck_pin),
static_cast<unsigned>(_miso_pin),
static_cast<unsigned>(_mosi_pin));
_spi = new SPIClass(_spi_bus);
if (_spi == nullptr) {
_mount_failed = true;
ARCHIVE_LOG("mount failed: SPI alloc failed");
return;
}
if (!mountArchiveSd(_spi, _spi_bus, _cs_pin, _sck_pin, _miso_pin, _mosi_pin)) {
_mount_failed = true;
ARCHIVE_LOG("mount failed bus=%u cs=%u sck=%u miso=%u mosi=%u",
static_cast<unsigned>(_spi_bus),
static_cast<unsigned>(_cs_pin),
static_cast<unsigned>(_sck_pin),
static_cast<unsigned>(_miso_pin),
static_cast<unsigned>(_mosi_pin));
return;
}
_mounted = true;
if (!SD.exists(getFsStatsPath())) {
SD.mkdir(getFsStatsPath());
}
refreshCardInfo();
ARCHIVE_LOG("mounted type=%s card=%llu total=%llu used=%llu path=%s",
getCardTypeName(),
static_cast<unsigned long long>(_card_size_bytes),
static_cast<unsigned long long>(_total_bytes),
static_cast<unsigned long long>(_used_bytes),
getLogicalStatsPath());
#else
_supported = false;
#endif
}
bool ArchiveStorage::recover() {
#if defined(ESP32)
if (!_supported || _spi == nullptr) {
return false;
}
ARCHIVE_LOG("recover begin bus=%u cs=%u", static_cast<unsigned>(_spi_bus), static_cast<unsigned>(_cs_pin));
SD.end();
_spi->end();
_mounted = false;
_mount_failed = false;
_card_size_bytes = 0;
_total_bytes = 0;
_used_bytes = 0;
if (!mountArchiveSd(_spi, _spi_bus, _cs_pin, _sck_pin, _miso_pin, _mosi_pin)) {
_mount_failed = true;
ARCHIVE_LOG("recover failed bus=%u cs=%u", static_cast<unsigned>(_spi_bus), static_cast<unsigned>(_cs_pin));
return false;
}
_mounted = true;
if (!SD.exists(getFsStatsPath())) {
SD.mkdir(getFsStatsPath());
}
refreshCardInfo();
ARCHIVE_LOG("recover mounted type=%s card=%llu total=%llu used=%llu path=%s",
getCardTypeName(),
static_cast<unsigned long long>(_card_size_bytes),
static_cast<unsigned long long>(_total_bytes),
static_cast<unsigned long long>(_used_bytes),
getLogicalStatsPath());
return true;
#else
return false;
#endif
}
bool ArchiveStorage::isSupported() const {
return _supported;
}
FILESYSTEM* ArchiveStorage::getFS() {
#if defined(ESP32)
return _mounted ? &SD : nullptr;
#else
return nullptr;
#endif
}
const char* ArchiveStorage::getCardTypeName() const {
#if defined(ESP32)
if (!_mounted) {
return "unavailable";
}
return cardTypeName(_card_type);
#else
return "unsupported";
#endif
}
#if defined(ESP32)
bool ArchiveStorage::resolvePins() {
#if defined(P_BOARD_SPI_SCK) && defined(P_BOARD_SPI_MISO) && defined(P_BOARD_SPI_MOSI) && defined(P_BOARD_SPI_CS)
_sck_pin = static_cast<uint8_t>(P_BOARD_SPI_SCK);
_miso_pin = static_cast<uint8_t>(P_BOARD_SPI_MISO);
_mosi_pin = static_cast<uint8_t>(P_BOARD_SPI_MOSI);
_cs_pin = static_cast<uint8_t>(P_BOARD_SPI_CS);
#if defined(TBEAM_SUPREME_SX1262) && defined(FSPI)
_spi_bus = FSPI;
#endif
return true;
#elif defined(TBEAM_SUPREME_SX1262)
// T-Beam S3 Supreme SD/IMU shared SPI bus pins.
_sck_pin = 36;
_miso_pin = 37;
_mosi_pin = 35;
_cs_pin = 47;
#if defined(FSPI)
_spi_bus = FSPI;
#endif
return true;
#elif defined(HAS_SDCARD) && defined(SDCARD_CS)
_cs_pin = static_cast<uint8_t>(SDCARD_CS);
#if defined(SPI_SCK) && defined(SPI_MISO) && defined(SPI_MOSI)
_sck_pin = static_cast<uint8_t>(SPI_SCK);
_miso_pin = static_cast<uint8_t>(SPI_MISO);
_mosi_pin = static_cast<uint8_t>(SPI_MOSI);
return true;
#elif defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI)
_sck_pin = static_cast<uint8_t>(P_LORA_SCLK);
_miso_pin = static_cast<uint8_t>(P_LORA_MISO);
_mosi_pin = static_cast<uint8_t>(P_LORA_MOSI);
return true;
#endif
#endif
return false;
}
void ArchiveStorage::refreshCardInfo() {
if (!_mounted) {
_card_type = 0;
_card_size_bytes = 0;
_total_bytes = 0;
_used_bytes = 0;
return;
}
if (_card_type == 0) {
_card_type = SD.cardType();
}
_card_size_bytes = SD.cardSize();
_total_bytes = SD.totalBytes();
_used_bytes = SD.usedBytes();
if (_total_bytes == 0) {
_total_bytes = _card_size_bytes;
}
}
#endif