diff --git a/include/graphics/common/SdCard.h b/include/graphics/common/SdCard.h index 17214db8..a40ae6a0 100644 --- a/include/graphics/common/SdCard.h +++ b/include/graphics/common/SdCard.h @@ -48,6 +48,20 @@ 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; } + + 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; } + // Release the card so it can be pulled safely. False when the card cannot + // be ejected (locally mounted cards are simply not held open). + virtual bool eject(void) { return false; } virtual uint64_t cardSize(void) = 0; virtual bool format(void) = 0; @@ -60,7 +74,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 +94,7 @@ class SDCard : public ISdCard virtual ~SDCard(void); }; -#elif defined(HAS_SDCARD) +#elif defined(HAS_SDCARD) && !defined(SENSECAP_INDICATOR) class SdFsCard : public ISdCard { public: @@ -96,6 +112,43 @@ 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 + { + if (info.present) + return eNoError; + // a card that is in there but has no filesystem can be formatted, + // just like a local one + return info.unformatted ? eFormatError : eSlotEmpty; + } + uint64_t usedBytes(void) override { return info.usedBytes; } + uint64_t freeBytes(void) override { return info.freeBytes; } + bool statsValid(void) override { return info.statsValid; } + StatsResult refreshStats(void) override; + bool eject(void) override; + uint64_t cardSize(void) override { return info.cardSize; } + bool format(void) override; + + 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..e7b6c1aa --- /dev/null +++ b/include/graphics/map/RemoteSDService.h @@ -0,0 +1,123 @@ +#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; + // usedBytes/freeBytes are only meaningful when true; the scan behind + // them runs in the background on the co-processor after mount + bool statsValid = false; + // a card is in the slot but carries no filesystem (present is false) + bool unformatted = false; +}; + +/** + * 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; + /** + * 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 + * 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 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; + /** + * Release the card so it can be pulled safely, or mount it again. The + * co-processor keeps it released until a mount is asked for. + */ + virtual bool sdEject(void) = 0; + virtual bool sdMount(void) = 0; + /** + * Put a fresh filesystem on the card, destroying what is on it. + */ + virtual bool sdFormat(void) = 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; + } RemoteFile; + + // One chunk for all open files: the image decoder opens the same tile + // three times and reads it front to back, so they all want the same bytes. + static uint8_t cachedChunk[CHUNK_SIZE]; + static char cachedPath[256]; + static uint32_t cachedOffset; + static uint32_t cachedLen; + static uint32_t cachedSize; + static uint32_t chunkAt(const char *path, uint32_t pos, uint32_t *fileSize); +}; diff --git a/include/graphics/view/TFT/TFTView_320x240.h b/include/graphics/view/TFT/TFTView_320x240.h index c2787374..8cf7af75 100644 --- a/include/graphics/view/TFT/TFTView_320x240.h +++ b/include/graphics/view/TFT/TFTView_320x240.h @@ -184,6 +184,16 @@ 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 a bounded number of times until they arrive + void refreshSDCardStats(void); + void armSDCardStatsPoll(void); + // release the card so it can be pulled safely; a tap mounts it again + void ejectSDCard(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/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/locale/en.yml b/locale/en.yml index 8c0a27bb..1465aa92 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -161,7 +161,9 @@ en: '%d new messages': ~ 'uptime: %02d:%02d:%02d': ~ "%s: %d GB (%s)\nUsed: %0.2f GB (%d%%)": ~ + 'Used: ...': ~ SD slot empty: ~ + SD ejected: ~ SD invalid format: ~ SD mbr not found: ~ SD card error: ~ diff --git a/source/graphics/TFT/TFTView_320x240.cpp b/source/graphics/TFT/TFTView_320x240.cpp index 3dfa3c17..9283a06d 100644 --- a/source/graphics/TFT/TFTView_320x240.cpp +++ b/source/graphics/TFT/TFTView_320x240.cpp @@ -43,6 +43,8 @@ fs::FS &fileSystem = LittleFS; #include "util/LinuxHelper.h" // #include "graphics/map/LinuxFileSystemService.h" #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 @@ -1510,6 +1512,11 @@ void TFTView_320x240::ui_event_SDCardButton(lv_event_t *e) if (THIS->formatSD) { ignoreClicked = true; THIS->formatSDCard(); + } else if (THIS->cardDetected) { + // release a healthy card so it can be pulled without corrupting + // it; a tap mounts it again + ignoreClicked = true; + THIS->ejectSDCard(); } } } @@ -2528,6 +2535,12 @@ void TFTView_320x240::loadMap(void) if (!map) { #if LV_USE_FS_ARDUINO_SD map = new MapPanel(objects.raw_map_panel); +#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); @@ -7217,13 +7230,16 @@ 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; } -#ifdef HAS_SDCARD +#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) char buf[64]; -#ifdef HAS_SD_MMC +#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; @@ -7231,33 +7247,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); - - 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); + cardDetected = true; + formatSDCardLabel(buf, sizeof(buf)); + if (!sdCard->statsValid()) { + // used/free are still being computed in the background on the + // 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) { @@ -7290,8 +7294,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 @@ -7307,18 +7316,102 @@ 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; + // 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); +} + +void TFTView_320x240::refreshSDCardStats(void) +{ +#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) + if (!sdCard || !cardDetected) + return; + 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); + 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()) { + // snprintf, not lv_snprintf: the LVGL one has no %f unless LVGL is + // built with float support, and prints the conversion verbatim + 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) { if (sdCard) { delete sdCard; sdCard = nullptr; } -#ifdef HAS_SDCARD -#ifdef HAS_SD_MMC +#if defined(SENSECAP_INDICATOR) + sdCard = new RemoteSdCard; +#elif defined(HAS_SD_MMC) sdCard = new SDCard; -#else +#elif defined(HAS_SDCARD) sdCard = new SdFsCard; #endif +#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) ILOG_DEBUG("formatting SD card"); if (sdCard->format()) { updateSDCard(); @@ -7330,6 +7423,28 @@ void TFTView_320x240::formatSDCard(void) sdCard = new NoSdCard; } +/** + * Release the card so it can be pulled without corrupting it. A tap on the + * button mounts whatever is in the slot again. + */ +void TFTView_320x240::ejectSDCard(void) +{ +#if defined(HAS_SDCARD) || defined(SENSECAP_INDICATOR) + if (!sdCard || !sdCard->eject()) + return; + ILOG_DEBUG("SD card ejected"); + cardDetected = false; + formatSD = false; + sdStatsPolls = 0; + delete sdCard; + sdCard = new NoSdCard; + lv_label_set_text(objects.home_sd_card_label, _("SD ejected")); + Themes::recolorButton(objects.home_sd_card_button, false); + Themes::recolorText(objects.home_sd_card_label, false); + lv_obj_add_state(objects.basic_settings_backup_restore_button, LV_STATE_DISABLED); +#endif +} + void TFTView_320x240::updateFreeMem(void) { // only update if HomePanel is active (since this is some critical code that did crash sporadically) diff --git a/source/graphics/common/SdCard.cpp b/source/graphics/common/SdCard.cpp index fab14566..afaf11be 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) { @@ -132,7 +134,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 +182,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 @@ -322,4 +326,152 @@ 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; + // a mount here is what makes the button revive a card that was ejected + // (or replaced) since the last look; mounting a mounted card is a no-op + fs->sdMount(); + if (!fs->sdInfo(info)) + return false; + return info.present; +} + +bool RemoteSdCard::eject(void) +{ + IRemoteFS *fs = RemoteSDService::backend(); + if (!fs || !fs->sdEject()) + return false; + info = RemoteSdInfo(); // gone until it is mounted again + return true; +} + +bool RemoteSdCard::format(void) +{ + IRemoteFS *fs = RemoteSDService::backend(); + if (!fs) + return false; + return fs->sdFormat(); +} + +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 eStatsUnavailable; + // takes the identity along: the card may have been swapped + info = fresh; + return info.statsValid ? eStatsValid : eStatsPending; +} + +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; + // 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); + } + } + 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"; + // 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[total] = '\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..7a0e8bef --- /dev/null +++ b/source/graphics/map/RemoteSDService.cpp @@ -0,0 +1,196 @@ +#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]; + // 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; + 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); + cachedLen = 0; // whatever we hold of this file is about to be stale + 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); + // 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; + } + return true; +} + +// One chunk, shared by all open files. The PNG decoder opens each tile three +// times (header, size, content) and reads it start to end, so a chunk fetched +// for one of those opens is what the next one asks for: keeping it here rather +// than per file saves fetching it again over the link. +uint8_t RemoteSDService::cachedChunk[RemoteSDService::CHUNK_SIZE]; +char RemoteSDService::cachedPath[256] = ""; +uint32_t RemoteSDService::cachedOffset = 0; +uint32_t RemoteSDService::cachedLen = 0; +uint32_t RemoteSDService::cachedSize = 0; + +// Fetches the chunk that holds `pos` unless it is the one we already have. +// Returns the number of bytes of it that follow pos, 0 on failure. +uint32_t RemoteSDService::chunkAt(const char *path, uint32_t pos, uint32_t *fileSize) +{ + uint32_t off = pos - (pos % CHUNK_SIZE); + if (cachedLen == 0 || off != cachedOffset || strcmp(cachedPath, path) != 0) { + uint32_t got = 0, size = 0; + if (!remoteFS || !remoteFS->readChunk(path, off, cachedChunk, CHUNK_SIZE, &got, &size) || got == 0) { + cachedLen = 0; + return 0; + } + strncpy(cachedPath, path, sizeof(cachedPath) - 1); + cachedPath[sizeof(cachedPath) - 1] = '\0'; + cachedOffset = off; + cachedLen = got; + cachedSize = size; + } + if (fileSize) + *fileSize = cachedSize; + if (pos >= cachedOffset + cachedLen) + return 0; // the chunk does not reach pos: file shrank under us + return cachedOffset + cachedLen - pos; +} + +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, sizeof(*rf)); + strncpy(rf->path, path, sizeof(rf->path) - 1); + + // The open has to fail for a file that is not there, and LVGL wants the + // size. Both come from the chunk holding offset 0, which the decoder is + // about to read anyway and which the next open of the same tile reuses. + if (chunkAt(rf->path, 0, &rf->size) == 0) { + 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) { + // avail is 0 when the chunk cannot be fetched, or does not reach pos + // because the file shrank since it was opened + uint32_t avail = chunkAt(rf->path, rf->pos, &rf->size); + if (avail == 0) + return LV_FS_RES_UNKNOWN; + uint32_t n = (btr < avail) ? btr : avail; + memcpy(dst, cachedChunk + (rf->pos - cachedOffset), 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: + // 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; + } + 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: