From dcb45b7d5ab6df2fcc2d66e10df7c2c0fcf3ff51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 17:10:19 +0200 Subject: [PATCH 1/6] Support SD cards behind a co-processor (SenseCAP Indicator) RemoteSDService implements an LVGL filesystem driver that fetches map tiles chunk-wise through an IRemoteFS backend registered by the firmware. RemoteSdCard provides map style detection, url providers and card statistics over the same backend. The I2CKeyboardScanner secondary bus is injectable so the bus 1 rescan can use a bridged TwoWire implementation instead of the local Wire1. --- include/graphics/common/SdCard.h | 27 +++++ include/graphics/map/RemoteSDService.h | 98 +++++++++++++++ include/input/I2CKeyboardScanner.h | 17 +++ source/graphics/TFT/TFTView_320x240.cpp | 17 ++- source/graphics/common/SdCard.cpp | 98 +++++++++++++++ source/graphics/map/RemoteSDService.cpp | 155 ++++++++++++++++++++++++ source/input/I2CKeyboardScanner.cpp | 23 +++- 7 files changed, 430 insertions(+), 5 deletions(-) create mode 100644 include/graphics/map/RemoteSDService.h create mode 100644 source/graphics/map/RemoteSDService.cpp diff --git a/include/graphics/common/SdCard.h b/include/graphics/common/SdCard.h index 17214db8..ef5f0e45 100644 --- a/include/graphics/common/SdCard.h +++ b/include/graphics/common/SdCard.h @@ -96,6 +96,33 @@ class SdFsCard : public ISdCard virtual ~SdFsCard(void) {} }; +#elif defined(SENSECAP_INDICATOR) +#include "graphics/map/RemoteSDService.h" + +/** + * SD card behind a co-processor, accessed through the RemoteSDService + * IRemoteFS backend. Statistics are fetched once over the link on init(). + */ +class RemoteSdCard : public ISdCard +{ + public: + bool init(void) override; + CardType cardType(void) override; + FatType fatType(void) override; + ErrorType errorType(void) override { return info.present ? eNoError : eSlotEmpty; } + uint64_t usedBytes(void) override { return info.usedBytes; } + uint64_t freeBytes(void) override { return info.freeBytes; } + uint64_t cardSize(void) override { return info.cardSize; } + bool format(void) override { return false; } + + std::set loadMapStyles(const char *folder) override; + std::string getUrlProvider(const char *folder, const char *style) override; + virtual ~RemoteSdCard(void) {} + + private: + RemoteSdInfo info; +}; + #endif /** diff --git a/include/graphics/map/RemoteSDService.h b/include/graphics/map/RemoteSDService.h new file mode 100644 index 00000000..62c4a857 --- /dev/null +++ b/include/graphics/map/RemoteSDService.h @@ -0,0 +1,98 @@ +#pragma once + +#include "graphics/map/TileService.h" +#include "lvgl.h" +#include +#include +#include + +/** + * Statistics of the remote SD card; numeric values match the + * meshtastic.SdCardInfo interdevice protobuf enums. + */ +struct RemoteSdInfo { + bool present = false; + uint8_t cardType = 0; // 0 none, 1 MMC, 2 SD, 3 SDHC, 4 SDXC, 5 unknown + uint8_t fatType = 0; // 0 unknown, 1 FAT16, 2 FAT32, 3 exFAT + uint64_t cardSize = 0; + uint64_t usedBytes = 0; + uint64_t freeBytes = 0; +}; + +/** + * Backend transport for RemoteSDService: chunked file access on a filesystem + * that lives on another MCU (e.g. the SD card behind the SenseCAP Indicator + * RP2040). Implemented and registered by the firmware before the view loads + * the map. + */ +class IRemoteFS +{ + public: + /** + * Read up to len bytes at offset. Returns false on transport error or + * missing file. *bytesRead receives the chunk size actually read, + * *fileSize the total size of the file. + */ + virtual bool readChunk(const char *path, uint32_t offset, uint8_t *buf, uint32_t len, uint32_t *bytesRead, + uint32_t *fileSize) = 0; + /** + * Sequential chunked write; create=true starts a new file (offset 0), + * create=false appends the chunk at offset == current file size. + */ + virtual bool writeChunk(const char *path, uint32_t offset, const uint8_t *buf, uint32_t len, bool create) = 0; + /** + * List all entries of a directory; subdirectories carry a trailing + * slash. Returns false on transport error or when path is not a + * directory. + */ + virtual bool listDir(const char *path, std::set &entries) = 0; + /** + * Card statistics, answered from a cache on the co-processor so the + * call stays fast. usedBytes/freeBytes may still read zero while the + * background FAT scan runs; a later call returns real values. + * Returns false on transport error. + */ + virtual bool sdInfo(RemoteSdInfo &info) = 0; + virtual ~IRemoteFS() {} +}; + +/** + * Map tile service for a remote SD card, accessed chunk-wise through an + * IRemoteFS backend. Registers an LVGL filesystem driver so the image + * decoder reads tiles like from a local drive. + */ +class RemoteSDService : public ITileService +{ + public: + static void setBackend(IRemoteFS *fs); + static IRemoteFS *backend(void); + + RemoteSDService(); + virtual ~RemoteSDService(); + + bool load(const char *name, void *img) override; + bool save(const char *name, void *img, size_t len) override; + + protected: + static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode); + static lv_fs_res_t fs_close(lv_fs_drv_t *drv, void *file_p); + static lv_fs_res_t fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br); + static lv_fs_res_t fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw); + static lv_fs_res_t fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence); + static lv_fs_res_t fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p); + + private: + static IRemoteFS *remoteFS; + + enum { CHUNK_SIZE = 4096 }; // matches FileTransfer.filedata max_size + + typedef struct RemoteFile { + char path[256]; // full FAT LFN paths round-trip + uint32_t pos; + uint32_t size; + // single chunk read-ahead cache + uint32_t chunkOffset; + uint32_t chunkLen; + uint8_t chunk[CHUNK_SIZE]; + } RemoteFile; +}; diff --git a/include/input/I2CKeyboardScanner.h b/include/input/I2CKeyboardScanner.h index 47451ea1..0689f538 100644 --- a/include/input/I2CKeyboardScanner.h +++ b/include/input/I2CKeyboardScanner.h @@ -1,5 +1,9 @@ #pragma once +#ifdef SENSECAP_INDICATOR +#include // TwoWire is a typedef on some cores, no forward declaration +#endif + class I2CKeyboardInputDriver; /** @@ -14,4 +18,17 @@ class I2CKeyboardScanner I2CKeyboardScanner(void); virtual I2CKeyboardInputDriver *scan(void); virtual ~I2CKeyboardScanner(void) {} + +#ifdef SENSECAP_INDICATOR + /** + * Override the bus used for the secondary (bus 1) keyboard scan, e.g. a + * bridged TwoWire implementation on devices whose second bus lives behind + * a co-processor. Must be set before input driver initialization; when + * unset the local Wire1 is used. + */ + static void setSecondaryBus(TwoWire *bus); + + private: + static TwoWire *secondaryBus; +#endif }; diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 72f662e8..147b86e9 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -45,6 +45,8 @@ fs::FS &fileSystem = LittleFS; #include "graphics/map/SDCardService.h" #elif defined(HAS_SD_MMC) #include "graphics/map/SDCardService.h" +#elif defined(SENSECAP_INDICATOR) +#include "graphics/map/RemoteSDService.h" #else #include "graphics/map/SdFatService.h" #endif @@ -2524,6 +2526,12 @@ void TFTView_320x240::loadMap(void) map = new MapPanel(objects.raw_map_panel, tileService); map->setBackupService( new URLService([tileService](const char *name, void *img, size_t len) { return tileService->save(name, img, len); })); +#elif defined(SENSECAP_INDICATOR) + // tiles live on the SD card behind the RP2040, fetched chunk-wise over the interdevice link + auto tileService = new RemoteSDService(); + map = new MapPanel(objects.raw_map_panel, tileService); + map->setBackupService( + new URLService([tileService](const char *name, void *img, size_t len) { return tileService->save(name, img, len); })); #elif defined(HAS_SDCARD) auto tileService = new SdFatService(); map = new MapPanel(objects.raw_map_panel, tileService); @@ -7212,10 +7220,12 @@ bool TFTView_320x240::updateSDCard(void) delete sdCard; sdCard = nullptr; } -#ifdef HAS_SDCARD +#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) char buf[64]; #ifdef HAS_SD_MMC sdCard = new SDCard; +#elif defined(SENSECAP_INDICATOR) + sdCard = new RemoteSdCard; // SD card behind the co-processor #else sdCard = new SdFsCard; #endif @@ -7281,8 +7291,13 @@ bool TFTView_320x240::updateSDCard(void) // allow backup/restore only if there is an SD card detected lv_obj_add_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); } else { +#if defined(SENSECAP_INDICATOR) + // backup/restore writes locally, which the bridged SD does not support yet + lv_obj_add_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); +#else // enable backup/restore lv_obj_clear_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); +#endif } lv_label_set_text(objects.home_sd_card_label, buf); #else diff --git a/source/graphics/common/SdCard.cpp b/source/graphics/common/SdCard.cpp index fab14566..4e1c02c7 100644 --- a/source/graphics/common/SdCard.cpp +++ b/source/graphics/common/SdCard.cpp @@ -322,4 +322,102 @@ std::string SdFsCard::getUrlProvider(const char *folder, const char *style) } return {}; } + +#elif defined(SENSECAP_INDICATOR) + +#include "graphics/map/RemoteSDService.h" +#include + +bool RemoteSdCard::init(void) +{ + IRemoteFS *fs = RemoteSDService::backend(); + if (!fs) + return false; + if (!fs->sdInfo(info)) + return false; + return info.present; +} + +ISdCard::CardType RemoteSdCard::cardType(void) +{ + // numeric values follow the meshtastic.SdCardInfo protobuf enum + switch (info.cardType) { + case 1: + return eMMC; + case 2: + return eSD; + case 3: + return eSDHC; + case 4: + return eSDXC; + case 5: + return eUnknown; + default: + return eNone; + } +} + +ISdCard::FatType RemoteSdCard::fatType(void) +{ + switch (info.fatType) { + case 1: + return eFat16; + case 2: + return eFat32; + case 3: + return eExFat; + default: + return eNA; + } +} + +std::set RemoteSdCard::loadMapStyles(const char *folder) +{ + std::set styles; + IRemoteFS *fs = RemoteSDService::backend(); + if (fs) { + std::set entries; + if (fs->listDir(folder, entries)) { + for (auto &entry : entries) { + if (entry.empty() || entry[0] == '.') + continue; + std::string dir = entry; + if (dir.back() == '/') // subdirectories carry a trailing slash + dir.pop_back(); + ILOG_DEBUG("remote SD: found map style: %s", dir.c_str()); + styles.insert(dir); + } + } + if (styles.empty()) { + std::set mapDir; + if (fs->listDir("/map", mapDir)) { + ILOG_DEBUG("remote SD: found /map dir"); + styles.insert("/map"); + } else { + ILOG_INFO("remote SD: no maps found"); + } + } + } + updated = true; + return styles; +} + +std::string RemoteSdCard::getUrlProvider(const char *folder, const char *style) +{ + IRemoteFS *fs = RemoteSDService::backend(); + if (!fs) + return {}; + std::string filename = std::string(folder) + "/" + style + "/.url"; + uint8_t buf[256]; + uint32_t bytesRead = 0, fileSize = 0; + if (!fs->readChunk(filename.c_str(), 0, buf, sizeof(buf) - 1, &bytesRead, &fileSize) || bytesRead == 0) + return {}; + buf[bytesRead] = '\0'; + // first line only + char *nl = strpbrk((char *)buf, "\r\n"); + if (nl) + *nl = '\0'; + return std::string{(char *)buf}; +} + #endif // HAS_SDCARD diff --git a/source/graphics/map/RemoteSDService.cpp b/source/graphics/map/RemoteSDService.cpp new file mode 100644 index 00000000..a45c280e --- /dev/null +++ b/source/graphics/map/RemoteSDService.cpp @@ -0,0 +1,155 @@ +#include "graphics/map/RemoteSDService.h" +#include "graphics/map/MapTileSettings.h" +#include "util/ILog.h" +#include +#include + +#define DRIVE_LETTER "S" + +IRemoteFS *RemoteSDService::remoteFS = nullptr; + +void RemoteSDService::setBackend(IRemoteFS *fs) +{ + remoteFS = fs; +} + +IRemoteFS *RemoteSDService::backend(void) +{ + return remoteFS; +} + +RemoteSDService::RemoteSDService() : ITileService(DRIVE_LETTER ":") +{ + static lv_fs_drv_t drv; + lv_fs_drv_init(&drv); + drv.letter = DRIVE_LETTER[0]; + drv.cache_size = MapTileSettings::getCacheSize(); + drv.ready_cb = nullptr; + drv.open_cb = fs_open; + drv.close_cb = fs_close; + drv.read_cb = fs_read; + drv.write_cb = fs_write; + drv.seek_cb = fs_seek; + drv.tell_cb = fs_tell; + lv_fs_drv_register(&drv); +} + +RemoteSDService::~RemoteSDService() {} + +bool RemoteSDService::load(const char *name, void *img) +{ + char buf[128]; + snprintf(buf, sizeof(buf), DRIVE_LETTER ":%s", name); + ILOG_DEBUG("RemoteSDService::load(): %s", buf); + lv_image_set_src((lv_obj_t *)img, buf); + // a failed set_src may keep the previous source, so verify the source + // now matches what was requested + const void *src = lv_image_get_src((lv_obj_t *)img); + if (!src || lv_image_src_get_type(src) != LV_IMAGE_SRC_FILE || strcmp((const char *)src, buf) != 0) { + ILOG_DEBUG("Failed to load tile %s from remote SD", buf); + return false; + } + return true; +} + +bool RemoteSDService::save(const char *name, void *img, size_t len) +{ + if (!remoteFS) + return false; + ILOG_DEBUG("RemoteSDService::save(%s): %d", name, len); + const uint8_t *data = static_cast(img); + uint32_t offset = 0; + while (offset < len) { + uint32_t chunk = (len - offset > CHUNK_SIZE) ? CHUNK_SIZE : len - offset; + if (!remoteFS->writeChunk(name, offset, data + offset, chunk, offset == 0)) { + ILOG_ERROR("failed to write %s at offset %u", name, offset); + return false; + } + offset += chunk; + } + return true; +} + +void *RemoteSDService::fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode) +{ + if (!remoteFS || mode != LV_FS_MODE_RD) + return nullptr; + + RemoteFile *rf = new (std::nothrow) RemoteFile; + if (!rf) + return nullptr; + memset(rf, 0, offsetof(RemoteFile, chunk)); + strncpy(rf->path, path, sizeof(rf->path) - 1); + + // fetch the first chunk right away: validates existence and returns the file size + if (!remoteFS->readChunk(rf->path, 0, rf->chunk, CHUNK_SIZE, &rf->chunkLen, &rf->size)) { + delete rf; + return nullptr; + } + return static_cast(rf); +} + +lv_fs_res_t RemoteSDService::fs_close(lv_fs_drv_t *drv, void *file_p) +{ + delete static_cast(file_p); + return LV_FS_RES_OK; +} + +lv_fs_res_t RemoteSDService::fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br) +{ + RemoteFile *rf = static_cast(file_p); + uint8_t *dst = static_cast(buf); + *br = 0; + + while (btr > 0 && rf->pos < rf->size) { + if (rf->pos < rf->chunkOffset || rf->pos >= rf->chunkOffset + rf->chunkLen) { + // fetch the chunk containing pos, aligned for cache friendliness on both ends + uint32_t off = rf->pos - (rf->pos % CHUNK_SIZE); + uint32_t got = 0, fsize = 0; + if (!remoteFS || !remoteFS->readChunk(rf->path, off, rf->chunk, CHUNK_SIZE, &got, &fsize) || got == 0) + return LV_FS_RES_UNKNOWN; // transport or backend error mid-file + rf->chunkOffset = off; + rf->chunkLen = got; + } + uint32_t avail = rf->chunkOffset + rf->chunkLen - rf->pos; + uint32_t n = (btr < avail) ? btr : avail; + memcpy(dst, rf->chunk + (rf->pos - rf->chunkOffset), n); + dst += n; + rf->pos += n; + *br += n; + btr -= n; + } + // reading zero bytes at end of file is a normal short read + return LV_FS_RES_OK; +} + +lv_fs_res_t RemoteSDService::fs_write(lv_fs_drv_t *drv, void *file_p, const void *buf, uint32_t btw, uint32_t *bw) +{ + *bw = 0; + return LV_FS_RES_NOT_IMP; +} + +lv_fs_res_t RemoteSDService::fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence) +{ + RemoteFile *rf = static_cast(file_p); + switch (whence) { + case LV_FS_SEEK_SET: + rf->pos = pos; + break; + case LV_FS_SEEK_CUR: + rf->pos += pos; + break; + case LV_FS_SEEK_END: + rf->pos = (pos <= rf->size) ? rf->size - pos : 0; + break; + default: + return LV_FS_RES_INV_PARAM; + } + return LV_FS_RES_OK; +} + +lv_fs_res_t RemoteSDService::fs_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p) +{ + *pos_p = static_cast(file_p)->pos; + return LV_FS_RES_OK; +} diff --git a/source/input/I2CKeyboardScanner.cpp b/source/input/I2CKeyboardScanner.cpp index ef3b83e8..747fb237 100644 --- a/source/input/I2CKeyboardScanner.cpp +++ b/source/input/I2CKeyboardScanner.cpp @@ -46,6 +46,15 @@ bool isDRV2605(TwoWire &bus, uint8_t address) } // namespace +#ifdef SENSECAP_INDICATOR +TwoWire *I2CKeyboardScanner::secondaryBus = nullptr; + +void I2CKeyboardScanner::setSecondaryBus(TwoWire *bus) +{ + secondaryBus = bus; +} +#endif + I2CKeyboardScanner::I2CKeyboardScanner(void) {} I2CKeyboardInputDriver *I2CKeyboardScanner::scan(void) @@ -115,18 +124,24 @@ I2CKeyboardInputDriver *I2CKeyboardScanner::scan(void) #if WIRE_INTERFACES_COUNT >= 2 if (driver == nullptr) { +#ifdef SENSECAP_INDICATOR + TwoWire &bus1 = secondaryBus ? *secondaryBus : Wire1; + ILOG_DEBUG("I2CKeyboardScanner scanning bus 1%s ...", secondaryBus ? " (bridged)" : ""); +#else + TwoWire &bus1 = Wire1; ILOG_DEBUG("I2CKeyboardScanner scanning bus 1 ..."); +#endif for (uint8_t i = 0; i < sizeof(i2cKeyboards_bus1); i++) { uint8_t address = i2cKeyboards_bus1[i]; - Wire1.beginTransmission(address); - if (Wire1.endTransmission() == 0) { + bus1.beginTransmission(address); + if (bus1.endTransmission() == 0) { switch (address) { case SCAN_CARDKB_ADDR: - driver = new CardKBInputDriver(address, Wire1); + driver = new CardKBInputDriver(address, bus1); break; case SCAN_TM9_KB_ADDR: #ifdef HAS_STC8H_KB - driver = new STC8HKeyboardInputDriver(address, Wire1); + driver = new STC8HKeyboardInputDriver(address, bus1); #endif break; default: From 27e6c0c5a6f280e67b910a6c17fbcf90768011ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 20:32:22 +0200 Subject: [PATCH 2/6] Address review: SENSECAP_INDICATOR branches take precedence over generic SD defines --- source/graphics/TFT/TFTView_320x240.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 147b86e9..2dc29fa6 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -43,10 +43,10 @@ fs::FS &fileSystem = LittleFS; #include "util/LinuxHelper.h" // #include "graphics/map/LinuxFileSystemService.h" #include "graphics/map/SDCardService.h" -#elif defined(HAS_SD_MMC) -#include "graphics/map/SDCardService.h" #elif defined(SENSECAP_INDICATOR) #include "graphics/map/RemoteSDService.h" +#elif defined(HAS_SD_MMC) +#include "graphics/map/SDCardService.h" #else #include "graphics/map/SdFatService.h" #endif @@ -2521,17 +2521,17 @@ void TFTView_320x240::loadMap(void) if (!map) { #if LV_USE_FS_ARDUINO_SD map = new MapPanel(objects.raw_map_panel); -#elif defined(HAS_SD_MMC) - auto tileService = new SDCardService(); - map = new MapPanel(objects.raw_map_panel, tileService); - map->setBackupService( - new URLService([tileService](const char *name, void *img, size_t len) { return tileService->save(name, img, len); })); #elif defined(SENSECAP_INDICATOR) // tiles live on the SD card behind the RP2040, fetched chunk-wise over the interdevice link auto tileService = new RemoteSDService(); map = new MapPanel(objects.raw_map_panel, tileService); map->setBackupService( new URLService([tileService](const char *name, void *img, size_t len) { return tileService->save(name, img, len); })); +#elif defined(HAS_SD_MMC) + auto tileService = new SDCardService(); + map = new MapPanel(objects.raw_map_panel, tileService); + map->setBackupService( + new URLService([tileService](const char *name, void *img, size_t len) { return tileService->save(name, img, len); })); #elif defined(HAS_SDCARD) auto tileService = new SdFatService(); map = new MapPanel(objects.raw_map_panel, tileService); @@ -7222,10 +7222,10 @@ bool TFTView_320x240::updateSDCard(void) } #if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) char buf[64]; -#ifdef HAS_SD_MMC - sdCard = new SDCard; -#elif defined(SENSECAP_INDICATOR) +#if defined(SENSECAP_INDICATOR) sdCard = new RemoteSdCard; // SD card behind the co-processor +#elif defined(HAS_SD_MMC) + sdCard = new SDCard; #else sdCard = new SdFsCard; #endif From 1dfc20403f419f1687a4fe3b64bfe1d78f8da7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 12 Jul 2026 22:21:22 +0200 Subject: [PATCH 3/6] Remote SD hardening: chunk coverage guard, no double transfer, stats validity fs_read verifies the fetched chunk covers the read position (a shrunk file would loop or over-read) and picks up size changes. The LVGL fs cache is disabled for the remote driver, its own chunk cache already serves header reads without pulling whole tiles twice over the UART. SEEK_END follows the LVGL size+pos convention. Failed multi-chunk saves delete the partial file via the new IRemoteFS remove operation. Map style listing only accepts directories. The .url provider reads until the first line is complete. SD stats display shows a placeholder while the co-processor scan is running (RemoteSdInfo.statsValid). SENSECAP branches take precedence over generic SD defines in SdCard.h/.cpp. --- include/graphics/common/SdCard.h | 10 +++++-- include/graphics/map/RemoteSDService.h | 14 ++++++++-- source/graphics/TFT/TFTView_320x240.cpp | 31 ++++++++++++--------- source/graphics/common/SdCard.cpp | 37 +++++++++++++++++++------ source/graphics/map/RemoteSDService.cpp | 18 ++++++++++-- 5 files changed, 81 insertions(+), 29 deletions(-) diff --git a/include/graphics/common/SdCard.h b/include/graphics/common/SdCard.h index ef5f0e45..1e29f360 100644 --- a/include/graphics/common/SdCard.h +++ b/include/graphics/common/SdCard.h @@ -48,6 +48,9 @@ class ISdCard virtual uint64_t usedBytes(void) = 0; virtual uint64_t freeBytes(void) = 0; + // false while used/free are not known yet (e.g. a background scan on a + // co-processor has not finished); local cards always know them + virtual bool statsValid(void) { return true; } virtual uint64_t cardSize(void) = 0; virtual bool format(void) = 0; @@ -60,7 +63,9 @@ class ISdCard bool updated = false; }; -#if defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) +// SENSECAP_INDICATOR takes precedence: the SD card sits behind the +// co-processor even when a generic SD define is also set +#if (defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC)) && !defined(SENSECAP_INDICATOR) class SDCard : public ISdCard { public: @@ -78,7 +83,7 @@ class SDCard : public ISdCard virtual ~SDCard(void); }; -#elif defined(HAS_SDCARD) +#elif defined(HAS_SDCARD) && !defined(SENSECAP_INDICATOR) class SdFsCard : public ISdCard { public: @@ -112,6 +117,7 @@ class RemoteSdCard : public ISdCard ErrorType errorType(void) override { return info.present ? eNoError : eSlotEmpty; } uint64_t usedBytes(void) override { return info.usedBytes; } uint64_t freeBytes(void) override { return info.freeBytes; } + bool statsValid(void) override { return info.statsValid; } uint64_t cardSize(void) override { return info.cardSize; } bool format(void) override { return false; } diff --git a/include/graphics/map/RemoteSDService.h b/include/graphics/map/RemoteSDService.h index 62c4a857..da3098d7 100644 --- a/include/graphics/map/RemoteSDService.h +++ b/include/graphics/map/RemoteSDService.h @@ -17,6 +17,9 @@ struct RemoteSdInfo { uint64_t cardSize = 0; uint64_t usedBytes = 0; uint64_t freeBytes = 0; + // usedBytes/freeBytes are only meaningful when true; the scan behind + // them runs in the background on the co-processor after mount + bool statsValid = false; }; /** @@ -40,6 +43,11 @@ class IRemoteFS * create=false appends the chunk at offset == current file size. */ virtual bool writeChunk(const char *path, uint32_t offset, const uint8_t *buf, uint32_t len, bool create) = 0; + /** + * Delete a file. Returns false on transport error or when the file + * does not exist. + */ + virtual bool remove(const char *path) = 0; /** * List all entries of a directory; subdirectories carry a trailing * slash. Returns false on transport error or when path is not a @@ -48,9 +56,9 @@ class IRemoteFS virtual bool listDir(const char *path, std::set &entries) = 0; /** * Card statistics, answered from a cache on the co-processor so the - * call stays fast. usedBytes/freeBytes may still read zero while the - * background FAT scan runs; a later call returns real values. - * Returns false on transport error. + * call stays fast. usedBytes/freeBytes carry real values once + * statsValid is true; a later call returns them when the background + * scan has finished. Returns false on transport error. */ virtual bool sdInfo(RemoteSdInfo &info) = 0; virtual ~IRemoteFS() {} diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 2dc29fa6..76a04e1d 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -7238,19 +7238,24 @@ bool TFTView_320x240::updateSDCard(void) uint32_t totalSpace = sdCard->cardSize() / (1024 * 1024); uint32_t totalSpaceGB = (sdCard->cardSize() + 500000000ULL) / (1000ULL * 1000ULL * 1000ULL); - sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), - cardType == ISdCard::eMMC ? "MMC" - : cardType == ISdCard::eSD ? "SDSC" - : cardType == ISdCard::eSDHC ? "SDHC" - : cardType == ISdCard::eSDXC ? "SDXC" - : "UNKN", - totalSpaceGB, - fatType == ISdCard::eExFat ? "exFAT" - : fatType == ISdCard::eFat32 ? "FAT32" - : fatType == ISdCard::eFat16 ? "FAT16" - : "???", - float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, - totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); + const char *cardTypeStr = cardType == ISdCard::eMMC ? "MMC" + : cardType == ISdCard::eSD ? "SDSC" + : cardType == ISdCard::eSDHC ? "SDHC" + : cardType == ISdCard::eSDXC ? "SDXC" + : "UNKN"; + const char *fatTypeStr = fatType == ISdCard::eExFat ? "exFAT" + : fatType == ISdCard::eFat32 ? "FAT32" + : fatType == ISdCard::eFat16 ? "FAT16" + : "???"; + if (sdCard->statsValid()) { + sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), cardTypeStr, totalSpaceGB, fatTypeStr, + float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, + totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); + } else { + // used/free are still being computed in the background on the + // co-processor; a later tap on the SD button shows real values + sprintf(buf, _("%s: %d GB (%s)\nUsed: ..."), cardTypeStr, totalSpaceGB, fatTypeStr); + } Themes::recolorButton(objects.home_sd_card_button, true); Themes::recolorText(objects.home_sd_card_label, true); cardDetected = true; diff --git a/source/graphics/common/SdCard.cpp b/source/graphics/common/SdCard.cpp index 4e1c02c7..8fdb0278 100644 --- a/source/graphics/common/SdCard.cpp +++ b/source/graphics/common/SdCard.cpp @@ -132,7 +132,9 @@ SDCard::~SDCard(void) } #endif -#if defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC) +// SENSECAP_INDICATOR takes precedence: the SD card sits behind the +// co-processor even when a generic SD define is also set +#if (defined(ARCH_PORTDUINO) || defined(HAS_SD_MMC)) && !defined(SENSECAP_INDICATOR) std::set SDCard::loadMapStyles(const char *folder) { std::set styles; @@ -178,7 +180,7 @@ std::string SDCard::getUrlProvider(const char *folder, const char *style) return {}; } -#elif defined(HAS_SDCARD) +#elif defined(HAS_SDCARD) && !defined(SENSECAP_INDICATOR) bool SdFsCard::init(void) { // TODO: allow specification of SPI bus @@ -381,9 +383,11 @@ std::set RemoteSdCard::loadMapStyles(const char *folder) for (auto &entry : entries) { if (entry.empty() || entry[0] == '.') continue; - std::string dir = entry; - if (dir.back() == '/') // subdirectories carry a trailing slash - dir.pop_back(); + // only subdirectories (trailing slash) are map styles; plain + // files in the maps folder must not show up in the selection + if (entry.back() != '/') + continue; + std::string dir = entry.substr(0, entry.size() - 1); ILOG_DEBUG("remote SD: found map style: %s", dir.c_str()); styles.insert(dir); } @@ -408,11 +412,26 @@ std::string RemoteSdCard::getUrlProvider(const char *folder, const char *style) if (!fs) return {}; std::string filename = std::string(folder) + "/" + style + "/.url"; - uint8_t buf[256]; - uint32_t bytesRead = 0, fileSize = 0; - if (!fs->readChunk(filename.c_str(), 0, buf, sizeof(buf) - 1, &bytesRead, &fileSize) || bytesRead == 0) + // read until the first line is complete instead of trusting a single + // chunk, so a long URL template does not get silently truncated + uint8_t buf[1024]; + uint32_t total = 0, fileSize = 0; + while (total < sizeof(buf) - 1) { + uint32_t bytesRead = 0; + if (!fs->readChunk(filename.c_str(), total, buf + total, sizeof(buf) - 1 - total, &bytesRead, &fileSize) || + bytesRead == 0) + break; + if (memchr(buf + total, '\n', bytesRead)) { + total += bytesRead; + break; + } + total += bytesRead; + if (fileSize > 0 && total >= fileSize) + break; + } + if (total == 0) return {}; - buf[bytesRead] = '\0'; + buf[total] = '\0'; // first line only char *nl = strpbrk((char *)buf, "\r\n"); if (nl) diff --git a/source/graphics/map/RemoteSDService.cpp b/source/graphics/map/RemoteSDService.cpp index a45c280e..fe10f7f2 100644 --- a/source/graphics/map/RemoteSDService.cpp +++ b/source/graphics/map/RemoteSDService.cpp @@ -23,7 +23,10 @@ RemoteSDService::RemoteSDService() : ITileService(DRIVE_LETTER ":") static lv_fs_drv_t drv; lv_fs_drv_init(&drv); drv.letter = DRIVE_LETTER[0]; - drv.cache_size = MapTileSettings::getCacheSize(); + // No LVGL-level cache: it would service small header reads by pulling + // cache_size bytes over the UART link, roughly doubling the transfer + // per tile. fs_read keeps its own chunk-sized read-ahead instead. + drv.cache_size = 0; drv.ready_cb = nullptr; drv.open_cb = fs_open; drv.close_cb = fs_close; @@ -63,6 +66,10 @@ bool RemoteSDService::save(const char *name, void *img, size_t len) uint32_t chunk = (len - offset > CHUNK_SIZE) ? CHUNK_SIZE : len - offset; if (!remoteFS->writeChunk(name, offset, data + offset, chunk, offset == 0)) { ILOG_ERROR("failed to write %s at offset %u", name, offset); + // don't leave a truncated tile behind: it would satisfy the + // existence check on the next load and never be re-fetched + if (offset > 0) + remoteFS->remove(name); return false; } offset += chunk; @@ -108,6 +115,11 @@ lv_fs_res_t RemoteSDService::fs_read(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t got = 0, fsize = 0; if (!remoteFS || !remoteFS->readChunk(rf->path, off, rf->chunk, CHUNK_SIZE, &got, &fsize) || got == 0) return LV_FS_RES_UNKNOWN; // transport or backend error mid-file + // A chunk that does not cover pos (file shrank or replaced since + // open) would loop forever or underflow the avail math below + if (got <= rf->pos - off) + return LV_FS_RES_UNKNOWN; + rf->size = fsize; // pick up a changed file size rf->chunkOffset = off; rf->chunkLen = got; } @@ -140,7 +152,9 @@ lv_fs_res_t RemoteSDService::fs_seek(lv_fs_drv_t *drv, void *file_p, uint32_t po rf->pos += pos; break; case LV_FS_SEEK_END: - rf->pos = (pos <= rf->size) ? rf->size - pos : 0; + // LVGL convention (matching its stdio/FATFS drivers): the position + // is size + pos; reads past the end return zero bytes + rf->pos = rf->size + pos; break; default: return LV_FS_RES_INV_PARAM; From 3775ba3d3ac6c3b5d3f60cf695c74d3ba9aa6d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 09:20:07 +0200 Subject: [PATCH 4/6] Refresh the SD stats display when the co-processor scan completes --- source/graphics/TFT/TFTView_320x240.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 76a04e1d..06b16931 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -7253,8 +7253,20 @@ bool TFTView_320x240::updateSDCard(void) totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); } else { // used/free are still being computed in the background on the - // co-processor; a later tap on the SD button shows real values + // co-processor; show a placeholder and poll again until the + // scan is done sprintf(buf, _("%s: %d GB (%s)\nUsed: ..."), cardTypeStr, totalSpaceGB, fatTypeStr); + static bool refreshPending = false; + if (!refreshPending) { + refreshPending = true; + lv_timer_t *refresh = lv_timer_create( + [](lv_timer_t *) { + refreshPending = false; + TFTView_320x240::instance()->updateSDCard(); + }, + 10 * 1000, NULL); + lv_timer_set_repeat_count(refresh, 1); + } } Themes::recolorButton(objects.home_sd_card_button, true); Themes::recolorText(objects.home_sd_card_label, true); From a4de10193446de3786c38b0e3370e1d09616bb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 10:06:08 +0200 Subject: [PATCH 5/6] Refresh SD statistics in place, keep SENSECAP ahead of the generic SD chain Polling the co-processor for the background computed used/free values no longer recreates the card object: that reset its updated flag, so the next loadMap() rescanned the styles with a stale prefix and could offer the zoom level directories of a flat /map card as map styles. The generic SD implementations are now excluded on SENSECAP_INDICATOR in the definition chain as well, matching their declarations. --- include/graphics/common/SdCard.h | 4 ++ include/graphics/view/TFT/TFTView_320x240.h | 4 ++ source/graphics/TFT/TFTView_320x240.cpp | 67 +++++++++++++++++---- source/graphics/common/SdCard.cpp | 20 +++++- 4 files changed, 80 insertions(+), 15 deletions(-) diff --git a/include/graphics/common/SdCard.h b/include/graphics/common/SdCard.h index 1e29f360..bc7c28ae 100644 --- a/include/graphics/common/SdCard.h +++ b/include/graphics/common/SdCard.h @@ -51,6 +51,9 @@ class ISdCard // false while used/free are not known yet (e.g. a background scan on a // co-processor has not finished); local cards always know them virtual bool statsValid(void) { return true; } + // Re-read used/free without re-detecting the card. Returns false while + // they are still unavailable, so the caller can poll. + virtual bool refreshStats(void) { return true; } virtual uint64_t cardSize(void) = 0; virtual bool format(void) = 0; @@ -118,6 +121,7 @@ class RemoteSdCard : public ISdCard uint64_t usedBytes(void) override { return info.usedBytes; } uint64_t freeBytes(void) override { return info.freeBytes; } bool statsValid(void) override { return info.statsValid; } + bool refreshStats(void) override; uint64_t cardSize(void) override { return info.cardSize; } bool format(void) override { return false; } diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index c2787374..94a39ef3 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -184,6 +184,10 @@ class TFTView_320x240 : public MeshtasticView virtual void updateTime(void); // update SD card slot info virtual bool updateSDCard(void); + // re-read only the card statistics (a co-processor may compute them in + // the background), polling until they are available + void refreshSDCardStats(void); + void armSDCardStatsPoll(void); // format SD card if invalid virtual void formatSDCard(void); // update time display on home screen diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 06b16931..616cfdef 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -7253,20 +7253,9 @@ bool TFTView_320x240::updateSDCard(void) totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); } else { // used/free are still being computed in the background on the - // co-processor; show a placeholder and poll again until the - // scan is done + // co-processor; show a placeholder and poll until they arrive sprintf(buf, _("%s: %d GB (%s)\nUsed: ..."), cardTypeStr, totalSpaceGB, fatTypeStr); - static bool refreshPending = false; - if (!refreshPending) { - refreshPending = true; - lv_timer_t *refresh = lv_timer_create( - [](lv_timer_t *) { - refreshPending = false; - TFTView_320x240::instance()->updateSDCard(); - }, - 10 * 1000, NULL); - lv_timer_set_repeat_count(refresh, 1); - } + armSDCardStatsPoll(); } Themes::recolorButton(objects.home_sd_card_button, true); Themes::recolorText(objects.home_sd_card_label, true); @@ -7330,6 +7319,58 @@ bool TFTView_320x240::updateSDCard(void) return cardDetected; } +/** + * Poll the card statistics until the co-processor has finished computing + * them. Only the numbers are re-read: recreating the card object would + * reset its updated flag and make the next loadMap() rescan the styles. + */ +void TFTView_320x240::armSDCardStatsPoll(void) +{ + static bool pollPending = false; + if (pollPending) + return; + pollPending = true; + lv_timer_t *poll = lv_timer_create( + [](lv_timer_t *) { + pollPending = false; + TFTView_320x240::instance()->refreshSDCardStats(); + }, + 10 * 1000, NULL); + lv_timer_set_repeat_count(poll, 1); +} + +void TFTView_320x240::refreshSDCardStats(void) +{ +#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) + if (!sdCard || !cardDetected) + return; + if (!sdCard->refreshStats()) { + armSDCardStatsPoll(); + return; + } + char buf[64]; + ISdCard::CardType cardType = sdCard->cardType(); + ISdCard::FatType fatType = sdCard->fatType(); + uint32_t usedSpace = sdCard->usedBytes() / (1024 * 1024); + uint32_t totalSpace = sdCard->cardSize() / (1024 * 1024); + uint32_t totalSpaceGB = (sdCard->cardSize() + 500000000ULL) / (1000ULL * 1000ULL * 1000ULL); + sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), + cardType == ISdCard::eMMC ? "MMC" + : cardType == ISdCard::eSD ? "SDSC" + : cardType == ISdCard::eSDHC ? "SDHC" + : cardType == ISdCard::eSDXC ? "SDXC" + : "UNKN", + totalSpaceGB, + fatType == ISdCard::eExFat ? "exFAT" + : fatType == ISdCard::eFat32 ? "FAT32" + : fatType == ISdCard::eFat16 ? "FAT16" + : "???", + float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, + totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); + lv_label_set_text(objects.home_sd_card_label, buf); +#endif +} + void TFTView_320x240::formatSDCard(void) { if (sdCard) { diff --git a/source/graphics/common/SdCard.cpp b/source/graphics/common/SdCard.cpp index 8fdb0278..6b304c22 100644 --- a/source/graphics/common/SdCard.cpp +++ b/source/graphics/common/SdCard.cpp @@ -24,7 +24,9 @@ using File = FsFile; ISdCard *sdCard = nullptr; -#if defined(ARCH_PORTDUINO) +// SENSECAP_INDICATOR takes precedence over the generic SD classes, matching +// the declarations in SdCard.h: the card sits behind the co-processor +#if defined(ARCH_PORTDUINO) && !defined(SENSECAP_INDICATOR) bool SDCard::init(void) { @@ -63,7 +65,7 @@ uint64_t SDCard::cardSize(void) SDCard::~SDCard(void) {} -#elif defined(HAS_SD_MMC) +#elif defined(HAS_SD_MMC) && !defined(SENSECAP_INDICATOR) bool SDCard::init(void) { @@ -340,6 +342,20 @@ bool RemoteSdCard::init(void) return info.present; } +bool RemoteSdCard::refreshStats(void) +{ + IRemoteFS *fs = RemoteSDService::backend(); + RemoteSdInfo fresh; + if (!fs || !fs->sdInfo(fresh) || !fresh.present) + return false; + // card identity does not change while it stays mounted, only the + // numbers the co-processor computes in the background + info.usedBytes = fresh.usedBytes; + info.freeBytes = fresh.freeBytes; + info.statsValid = fresh.statsValid; + return info.statsValid; +} + ISdCard::CardType RemoteSdCard::cardType(void) { // numeric values follow the meshtastic.SdCardInfo protobuf enum From f74a67f2ae97f83d1d36db3fc1dfbe766c0cf5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 13 Jul 2026 10:36:44 +0200 Subject: [PATCH 6/6] Stop polling SD statistics when the card is gone The poll treated every failure as a scan that had not finished, so a removed card or a down link made it retry forever, blocking the UI thread on a link round trip each time. refreshStats now reports whether the statistics are pending or unavailable, the poll is bounded, and a card that disappears re-runs the detection instead of painting statistics over the error. --- include/graphics/common/SdCard.h | 13 ++- include/graphics/view/TFT/TFTView_320x240.h | 6 +- locale/en.yml | 1 + source/graphics/TFT/TFTView_320x240.cpp | 93 ++++++++++++--------- source/graphics/common/SdCard.cpp | 15 ++-- 5 files changed, 74 insertions(+), 54 deletions(-) diff --git a/include/graphics/common/SdCard.h b/include/graphics/common/SdCard.h index bc7c28ae..40bfc760 100644 --- a/include/graphics/common/SdCard.h +++ b/include/graphics/common/SdCard.h @@ -51,9 +51,14 @@ class ISdCard // false while used/free are not known yet (e.g. a background scan on a // co-processor has not finished); local cards always know them virtual bool statsValid(void) { return true; } - // Re-read used/free without re-detecting the card. Returns false while - // they are still unavailable, so the caller can poll. - virtual bool refreshStats(void) { return true; } + + enum StatsResult { + eStatsValid, // used/free are up to date + eStatsPending, // still being computed, ask again later + eStatsUnavailable, // card or link gone, stop asking + }; + // Re-read used/free without re-detecting the card + virtual StatsResult refreshStats(void) { return eStatsValid; } virtual uint64_t cardSize(void) = 0; virtual bool format(void) = 0; @@ -121,7 +126,7 @@ class RemoteSdCard : public ISdCard uint64_t usedBytes(void) override { return info.usedBytes; } uint64_t freeBytes(void) override { return info.freeBytes; } bool statsValid(void) override { return info.statsValid; } - bool refreshStats(void) override; + StatsResult refreshStats(void) override; uint64_t cardSize(void) override { return info.cardSize; } bool format(void) override { return false; } diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index 94a39ef3..743fba51 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -185,9 +185,13 @@ class TFTView_320x240 : public MeshtasticView // update SD card slot info virtual bool updateSDCard(void); // re-read only the card statistics (a co-processor may compute them in - // the background), polling until they are available + // the background), polling a bounded number of times until they arrive void refreshSDCardStats(void); void armSDCardStatsPoll(void); +#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) + void formatSDCardLabel(char *buf, size_t len); +#endif + uint16_t sdStatsPolls = 0; // format SD card if invalid virtual void formatSDCard(void); // update time display on home screen diff --git a/locale/en.yml b/locale/en.yml index 8c0a27bb..cc6df6af 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -161,6 +161,7 @@ en: '%d new messages': ~ 'uptime: %02d:%02d:%02d': ~ "%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)": ~ + 'Used: ...': ~ SD slot empty: ~ SD invalid format: ~ SD mbr not found: ~ diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 616cfdef..0c29dd65 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -7216,6 +7216,7 @@ void TFTView_320x240::updateTime(void) bool TFTView_320x240::updateSDCard(void) { formatSD = false; + sdStatsPolls = 0; // a fresh detection gets a fresh poll budget if (sdCard) { delete sdCard; sdCard = nullptr; @@ -7232,39 +7233,21 @@ bool TFTView_320x240::updateSDCard(void) ISdCard::ErrorType err = ISdCard::ErrorType::eNoError; if (sdCard->init() && sdCard->cardType() != ISdCard::eNone) { ILOG_DEBUG("SdCard init successful, card type: %d", sdCard->cardType()); - ISdCard::CardType cardType = sdCard->cardType(); - ISdCard::FatType fatType = sdCard->fatType(); - uint32_t usedSpace = sdCard->usedBytes() / (1024 * 1024); - uint32_t totalSpace = sdCard->cardSize() / (1024 * 1024); - uint32_t totalSpaceGB = (sdCard->cardSize() + 500000000ULL) / (1000ULL * 1000ULL * 1000ULL); - - const char *cardTypeStr = cardType == ISdCard::eMMC ? "MMC" - : cardType == ISdCard::eSD ? "SDSC" - : cardType == ISdCard::eSDHC ? "SDHC" - : cardType == ISdCard::eSDXC ? "SDXC" - : "UNKN"; - const char *fatTypeStr = fatType == ISdCard::eExFat ? "exFAT" - : fatType == ISdCard::eFat32 ? "FAT32" - : fatType == ISdCard::eFat16 ? "FAT16" - : "???"; - if (sdCard->statsValid()) { - sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), cardTypeStr, totalSpaceGB, fatTypeStr, - float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, - totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); - } else { + cardDetected = true; + formatSDCardLabel(buf, sizeof(buf)); + if (!sdCard->statsValid()) { // used/free are still being computed in the background on the - // co-processor; show a placeholder and poll until they arrive - sprintf(buf, _("%s: %d GB (%s)\nUsed: ..."), cardTypeStr, totalSpaceGB, fatTypeStr); + // co-processor; the label shows a placeholder until they arrive armSDCardStatsPoll(); } Themes::recolorButton(objects.home_sd_card_button, true); Themes::recolorText(objects.home_sd_card_label, true); - cardDetected = true; } else { - ILOG_DEBUG("SdFsCard init failed"); + ILOG_DEBUG("SdCard init failed"); err = sdCard->errorType(); delete sdCard; sdCard = nullptr; + cardDetected = false; // a poll must not paint stats over the error } if (!cardDetected || err != ISdCard::ErrorType::eNoError) { @@ -7329,13 +7312,21 @@ void TFTView_320x240::armSDCardStatsPoll(void) static bool pollPending = false; if (pollPending) return; - pollPending = true; + // a scan of a large card takes a while, but not forever: give up rather + // than block the UI thread with a link round trip every 10s for good + if (++sdStatsPolls > 30) { + ILOG_WARN("SD card statistics never became available"); + return; + } lv_timer_t *poll = lv_timer_create( [](lv_timer_t *) { pollPending = false; TFTView_320x240::instance()->refreshSDCardStats(); }, 10 * 1000, NULL); + if (!poll) + return; // out of timers, the label just keeps its placeholder + pollPending = true; lv_timer_set_repeat_count(poll, 1); } @@ -7344,32 +7335,52 @@ void TFTView_320x240::refreshSDCardStats(void) #if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) if (!sdCard || !cardDetected) return; - if (!sdCard->refreshStats()) { + switch (sdCard->refreshStats()) { + case ISdCard::eStatsPending: armSDCardStatsPoll(); return; + case ISdCard::eStatsUnavailable: + // the card is gone or the link is down: re-detect, which paints the + // proper error state instead of stale numbers + updateSDCard(); + return; + case ISdCard::eStatsValid: + break; } char buf[64]; + formatSDCardLabel(buf, sizeof(buf)); + lv_label_set_text(objects.home_sd_card_label, buf); +#endif +} + +#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) +// caller guarantees a detected card; used/free are only printed once the +// card knows them (a co-processor computes them in the background) +void TFTView_320x240::formatSDCardLabel(char *buf, size_t len) +{ ISdCard::CardType cardType = sdCard->cardType(); ISdCard::FatType fatType = sdCard->fatType(); uint32_t usedSpace = sdCard->usedBytes() / (1024 * 1024); uint32_t totalSpace = sdCard->cardSize() / (1024 * 1024); uint32_t totalSpaceGB = (sdCard->cardSize() + 500000000ULL) / (1000ULL * 1000ULL * 1000ULL); - sprintf(buf, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), - cardType == ISdCard::eMMC ? "MMC" - : cardType == ISdCard::eSD ? "SDSC" - : cardType == ISdCard::eSDHC ? "SDHC" - : cardType == ISdCard::eSDXC ? "SDXC" - : "UNKN", - totalSpaceGB, - fatType == ISdCard::eExFat ? "exFAT" - : fatType == ISdCard::eFat32 ? "FAT32" - : fatType == ISdCard::eFat16 ? "FAT16" - : "???", - float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, - totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); - lv_label_set_text(objects.home_sd_card_label, buf); -#endif + const char *cardTypeStr = cardType == ISdCard::eMMC ? "MMC" + : cardType == ISdCard::eSD ? "SDSC" + : cardType == ISdCard::eSDHC ? "SDHC" + : cardType == ISdCard::eSDXC ? "SDXC" + : "UNKN"; + const char *fatTypeStr = fatType == ISdCard::eExFat ? "exFAT" + : fatType == ISdCard::eFat32 ? "FAT32" + : fatType == ISdCard::eFat16 ? "FAT16" + : "???"; + if (sdCard->statsValid()) { + lv_snprintf(buf, len, _("%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)"), cardTypeStr, totalSpaceGB, fatTypeStr, + float(sdCard->usedBytes()) / 1024.0f / 1024.0f / 1024.0f, + totalSpace ? ((usedSpace * 100) + totalSpace / 2) / totalSpace : 0); + } else { + lv_snprintf(buf, len, "%s: %d GB (%s)\n%s", cardTypeStr, totalSpaceGB, fatTypeStr, _("Used: ...")); + } } +#endif void TFTView_320x240::formatSDCard(void) { diff --git a/source/graphics/common/SdCard.cpp b/source/graphics/common/SdCard.cpp index 6b304c22..a5512f58 100644 --- a/source/graphics/common/SdCard.cpp +++ b/source/graphics/common/SdCard.cpp @@ -342,18 +342,17 @@ bool RemoteSdCard::init(void) return info.present; } -bool RemoteSdCard::refreshStats(void) +ISdCard::StatsResult RemoteSdCard::refreshStats(void) { IRemoteFS *fs = RemoteSDService::backend(); RemoteSdInfo fresh; + // a card that is gone (or a link that is down) is not a pending scan: + // the caller must stop polling and re-detect instead if (!fs || !fs->sdInfo(fresh) || !fresh.present) - return false; - // card identity does not change while it stays mounted, only the - // numbers the co-processor computes in the background - info.usedBytes = fresh.usedBytes; - info.freeBytes = fresh.freeBytes; - info.statsValid = fresh.statsValid; - return info.statsValid; + return eStatsUnavailable; + // takes the identity along: the card may have been swapped + info = fresh; + return info.statsValid ? eStatsValid : eStatsPending; } ISdCard::CardType RemoteSdCard::cardType(void)